日志及重构

989577
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:日志级别

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_clientlog_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文档可以知道,它定义了一系列方法: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 的实现。