本章我们将对之前的代码进行重构并且使用日志记录可能发生的错误。
本章代码在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
:日志级别
之后,我们在main
函数增加了一条info
级别的日志,用来打印当前应用监听的地址:
tracing::info!("服务器监听于:{}", &cfg.web.addr);
重构 handler
模块
在 handler 中,所有函数都要从共享状态的连接池中获取连接,还要对可能出现的错误转换成我们自己的 AppError
。我们将这个功能抽取为一个函数,同时对可能出现的错误进行日志记录。
定义 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
:
参数
它的参数很简单的,就是 handler 的名称
返回值
-
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
模块
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 的实现。