本章代码在05/日志及重构分支。
初始化日志
首先,我们在main
函数中对日志进行初始化:
// 初始化日志
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "todo=debug");
}
tracing_subscriber::fmt::init();
我们先检测是否设置过RUST_LOG
这个环境变量,如果没有设置,将其设置为 todo=debug
。
-
todo
: crate 的名字,对于当前项目而言,它应该和Cargo.toml
定义的name
属性一样。 -
debug
:日志级别
tracing::info!("服务器监听于:{}", &cfg.web.addr);
重构 handler
模块
在 handler 中,所有函数都要从共享状态的连接池中获取连接,还要对可能出现的错误转换成我们自己的 AppError
。我们将这个功能抽取为一个函数,同时对可能出现的错误进行日志记录。
定义 get_client
函数
我们在 src/handler/mod.rs
中定义 get_client
:
async fn get_client(state: &AppState, handler_name: &str) -> Result<Client> {
state.pool.get().await.map_err(|err| {
tracing::error!("{}: {:?}", handler_name, err);
AppError::db_error(err)
})
}
-
tracing::error!("{}: {:?}", handler_name, err);
:记录发生的原始错误(PoolError
) -
AppError::db_error(err)
:将原始错误转换成AppError
参数
-
state: &AppState
:共享状态的引用,其中包含了数据库连接池 -
handler_name: &str
:为了明确标识可能发生的错误在哪个 handler 中发生,我们附加了这个参数
返回值
如果没有错误发生,返回 Client
对象;否则,返回 AppError
。
在 handler 中使用
现在,我们可以在 handler 函数中使用这个 get_client
了,比如:
pub async fn create(
Extension(state): Extension<AppState>,
Json(payload): Json<form::CreateTodoList>,
) -> HandlerResult<TodoListID> {
let handler_name = "todo_list_create";
let client = get_client(&state, handler_name).await?;
// ...
}
记录数据库操作可能发生的错误
在之前的代码中,我们将数据库操作可能发生的错误直接返回:
let result = todo_list::create(&client, payload).await?;
现在我们需要把这个可能发生的错误记录到日志中。
比如,我们可以参照get_client
这样写:
let result = todo_list::create(&client, payload).await.map_err(|err| {
tracing::error!("{}: {:?}", handler_name, err);
err
});
因为db::*
里的所有函数,我们都已经转成了 AppError
,所以在这个 map_err
回调中,我们直接返回这个错误就行。
问题来了,每个数据库操作都要写这么一个闭包,我还是把这个闭包提取为一个函数。
定义 log_error
函数
我们在 src/handler/mod.rs
中定义 log_error
:
fn log_error(handler_name: String) -> Box<dyn Fn(AppError) -> AppError> {
Box::new(move |err| {
tracing::debug!("{}: {:?}", handler_name, err);
err
})
}
参数
返回值
-
Fn(AppError) -> AppError
:这是一个闭包声明,它接收一个AppError
参数,然后返回一个AppError
-
Box<dyn 闭包声明>
:由于闭包声明是一个未知大小的,所需要配合dyn
关键字,并使用Box
智能指针,让它的大写变成固定
在 handler 中使用
现在我们可以在 handler 函数中使用了:
pub async fn create(
Extension(state): Extension<AppState>,
Json(payload): Json<form::CreateTodoList>,
) -> HandlerResult<TodoListID> {
// ...
let result = todo_list::create(&client, payload)
.await
.map_err(log_error(handler_name.to_string()))?;
// ...
}
学习到这里,先暂停,把所有 handler 函数改用 get_client
和 log_error
。
重构 db
模块
来看一下目前 db
里的相关函数,以 todo_list::create()
和 todo_list::update()
为例:
pub async fn create(client: &Client, frm: form::CreateTodoList) -> Result<TodoListID> {
let stmt = client
.prepare("INSERT INTO todo_list (title) VALUES ($1) RETURNING id")
.await
.map_err(AppError::from)?;
let result = client
.query(&stmt, &[&frm.title])
.await
.map_err(AppError::from)?
.iter()
.map(|row| TodoListID::from_row_ref(row).unwrap())
.collect::<Vec<TodoListID>>()
.pop()
.ok_or(AppError::not_found())?;
Ok(result)
}
pub async fn update(client: &Client, frm: form::UpdateTodoList) -> Result<bool> {
let stmt = client
.prepare("UPDATE todo_list SET title=$1 WHERE id=$2")
.await
.map_err(AppError::from)?;
let result = client
.execute(&stmt, &[&frm.title, &frm.id])
.await
.map_err(AppError::from)?;
Ok(result > 0)
}
可以看到,获取 stmt
的代码是重复的,完全可以提取出来。
定义 get_stmt
函数
第一步,我们在 src/db/mod.rs
定义 get_stmt
函数:
async fn get_stmt<C: GenericClient>(client: &C, sql: &str) -> Result<Statement> {
client.prepare(sql).await.map_err(AppError::from)
}
等等,这个泛型C
是什么鬼?查看GenericClient文档可以知道,它定义了一系列方法:prepare
、query
……这些不就是我们之前用的 Client
和事务的方法吗?所以,我们大胆猜测,Client
和事务应该实现了这个 trait。这正是我们想要的,因为我们期望的是,我们的get_stmt
既可以处理Client
又可以处理事务。
抽取 query
函数
这个函数用于查找多条记录。
async fn query<T, C>(client: &C, sql: &str, args: &[&(dyn ToSql + Sync)]) -> Result<Vec<T>>
where
C: GenericClient,
T: FromTokioPostgresRow,
{
let stmt = get_stmt(client, sql).await?;
let result = client
.query(&stmt, args)
.await
.map_err(AppError::from)?
.iter()
.map(|row| <T>::from_row_ref(row).unwrap())
.collect::<Vec<T>>();
Ok(result)
}
为了使用 from_row_ref()
,我们需要限定泛型T
必须实现 tokio_pg_mapper::FromTokioPostgresRow
。由于我们定义模型的时候,使用了 tokio_pg_mapper_derive::PostgresMapper
这个 derive,它自动帮我们实现了 tokio_pg_mapper::FromTokioPostgresRow
。
抽取 query_one
函数
这个函数用于查找单条记录。注意,tokio_postgres
提供了多种查找单条记录的方法,而我们使用方法是,使用查找多条记录的方法,然后调用 pop()
获取单条记录。
async fn query_one<T, C>(client: &C, sql: &str, args: &[&(dyn ToSql + Sync)]) -> Result<T>
where
C: GenericClient,
T: FromTokioPostgresRow,
{
let result: T = query(client, sql, args)
.await?
.pop()
.ok_or(AppError::not_found())?;
Ok(result)
}
抽取 execute
函数
async fn execute<C: GenericClient>(
client: &C,
sql: &str,
args: &[&(dyn ToSql + Sync)],
) -> Result<u64> {
let stmt = get_stmt(client, sql).await?;
let rows = client.execute(&stmt, args).await.map_err(AppError::from)?;
Ok(rows)
}
使用这些抽取出来的函数
下面分别演示如何使用这些函数。特别注意,你需要把 use deadpool_postgres::Client;
改为 use tokio_postgres::Client;
。
-
为什么需要改用
tokio_postgres::Client
?——因为它才实现了GenericClient
trait -
为什么只需要改这个
use
语句,其它代码不用改?——因为deadpool_postgres::Client
包装了tokio_postgres::Client
create()
的重构
pub async fn create(client: &Client, frm: form::CreateTodoList) -> Result<TodoListID> {
let result: TodoListID = super::query_one(
client,
"INSERT INTO todo_list (title) VALUES ($1) RETURNING id",
&[&frm.title],
)
.await?;
Ok(result)
}
all()
的重构
pub async fn all(client: &Client) -> Result<Vec<TodoList>> {
let result: Vec<TodoList> = super::query(
client,
"SELECT id,title FROM todo_list ORDER BY id DESC",
&[],
)
.await?;
Ok(result)
}
find()
的重构
pub async fn find(client: &Client, list_id: i32) -> Result<TodoList> {
let result: TodoList = super::query_one(
client,
"SELECT id,title FROM todo_list WHERE id=$1 LIMIT 1",
&[&list_id],
)
.await?;
Ok(result)
}
事务的重构
pub async fn delete(client: &mut Client, id: i32) -> Result<bool> {
let tx = client.transaction().await.map_err(AppError::from)?;
let result = super::execute(&tx, "DELETE FROM todo_list WHERE id=$1", &[&id]).await;
if let Err(err) = result {
tx.rollback().await.map_err(AppError::from)?;
return Err(AppError::db_error(err));
};
let result = super::execute(&tx, "DELETE FROM todo_item WHERE list_id=$1", &[&id]).await;
if let Err(err) = result {
tx.rollback().await.map_err(AppError::from)?;
return Err(AppError::db_error(err));
};
tx.commit().await.map_err(AppError::from)?;
Ok(true)
}
至此,重构已经完成。下面我们完成最后一步:TodoItem 的实现。