日志及重构

989529
2021/11/25 18:33:37

本章我们将对之前的代码进行重构并且使用日志记录可能发生的错误。

本章代码在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_clientlog_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文档可以知道,它定义了一系列方法:preparequery……这些不就是我们之前用的 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 的实现。