数据库、模型、状态共享及TodoList

1373684
2021/11/25 18:31:46

现在是时候开始进行数据库操作,以便实现功能了。本章将实现TodoList的功能。

增加数据库配置

/// 应用配置
#[derive(Deserialize)]
pub struct Config {
    pub web: WebConfig,
    pub pg: deadpool_postgres::Config,
}

状态共享

为了在整个应用间共享数据库连接池,我们需要定义状态共享

/// 应用状态共享
#[derive(Clone)]
pub struct AppState {
    /// PostgreSQL 连接池
    pub pool: deadpool_postgres::Pool,
}

同时,我们需要在main函数中实例化增加这个共享

#[tokio::main]
async fn main() {
    // ...
    let pool = cfg
        .pg
        .create_pool(tokio_postgres::NoTls)
        .expect("初始化数据库连接池失败");

    let app = Router::new()
        // ...
        .layer(AddExtensionLayer::new(AppState { pool }));

    // ...
}

TodoList 模型

我们定义两个TodoList 相关的模型

  • TodoList:用于映射完整的 TodoList

  • TodoListID:用于映射新插入的 TodoList 的 ID

TodoList 数据库操作

src/db/todo_list.rs中,我们为 TodoList 实现数据库相关操作:

create:创建一个 TodoList

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)
}

这里使用了一个名为CreateTodoList的结构体,它定义在form模块:

#[derive(Deserialize)]
pub struct CreateTodoList {
    pub title: String,
}

all:所有 TodoList 的列表

pub async fn all(client: &Client) -> Result<Vec<TodoList>> {
    let stmt = client
        .prepare("SELECT id,title FROM todo_list ORDER BY id DESC")
        .await
        .map_err(AppError::from)?;
    let result = client
        .query(&stmt, &[])
        .await
        .map_err(AppError::from)?
        .iter()
        .map(|row| TodoList::from_row_ref(row).unwrap())
        .collect::<Vec<TodoList>>();
    Ok(result)
}

find:根据 ID 查找 TodoList

pub async fn find(client: &Client, list_id: i32) -> Result<TodoList> {
    let stmt = client
        .prepare("SELECT id,title FROM todo_list WHERE id=$1")
        .await
        .map_err(AppError::from)?;
    let result = client
        .query(&stmt, &[&list_id])
        .await
        .map_err(AppError::from)?
        .iter()
        .map(|row| TodoList::from_row_ref(row).unwrap())
        .collect::<Vec<TodoList>>()
        .pop()
        .ok_or(AppError::not_found())?;
    Ok(result)
}

update:修改 TodoList

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)
}

这里使用了一个名为 UpdateTodoList 的结构体,它定义在 form 模块:

/// 修改待办列表
#[derive(Deserialize)]
pub struct UpdateTodoList {
    pub id: i32,
    pub title: String,
}

delete:删除 TodoList 及其相关的 TodoItem

pub async fn delete(client: &mut Client, id: i32) -> Result<bool> {
    let tx = client.transaction().await.map_err(AppError::from)?;
    let stmt = tx
        .prepare("DELETE FROM todo_list  WHERE id=$1")
        .await
        .map_err(AppError::from)?;
    let result = tx.execute(&stmt, &[&id]).await;
    if let Err(err) = result {
        tx.rollback().await.map_err(AppError::from)?;
        return Err(AppError::db_error(err));
    };
    let stmt = tx
        .prepare("DELETE FROM todo_item WHERE list_id=$1")
        .await
        .map_err(AppError::from)?;
    let result = tx.execute(&stmt, &[&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)
}

由于要同时对两个表进行操作,为了保证操作的原子性,这里使用了事务。

TodoList 的 handler

首先,我们对handler模块进行了拆分,把之前的src/handle.rs单一文件,拆分成了目录,并在其中增加了todo_list模块。

下面以createfind来对 handler 进行简要说明。

create 函数

参数

  • Extension(state): Extension<AppState>:获取共享状态,里面包含有数据库连接池

  • Json(payload): Json<form::CreateTodoList>:以 JSON 方式从客户端获取输入的数据,并将其反序化成 CreateTodoList 结构体

返回值

  • crate::Result:这是我们自定义的Result,再次提示,它包含了我们自定义错误AppError

  • Json:axum 提供的功能,将泛型的数据自定义序列化成 JSON 格式

  • Response:我们在上一章定义的响应

  • TodoListID:里面包含了新增加的 TodoList 的 ID

错误转换

在获取数据库客户端时,我们使用了map_err将某个类型的错误转换成了AppError,这是 Rust 编程中的常用技巧。

pub async fn create(
    Extension(state): Extension<AppState>,
    Json(payload): Json<form::CreateTodoList>,
) -> Result<Json<Response<TodoListID>>> {
    let client = state.pool.get().await.map_err(AppError::from)?;
    let result = todo_list::create(&client, payload).await?;
    Ok(Json(Response::ok(result)))
}

find 函数

参数

除了共享状态之外,我们还通过 Path来获取到了客户端传递过来的 TodoList 的 ID:

Path(list_id): Path<i32>

返回值

这次,我们要返回的是TodoList,它包含了完整的 TodoList 数据。

pub async fn find(
    Extension(state): Extension<AppState>,
    Path(list_id): Path<i32>,
) -> Result<Json<Response<TodoList>>> {
    let client = state.pool.get().await.map_err(AppError::from)?;
    let result = todo_list::find(&client, list_id).await?;
    Ok(Json(Response::ok(result)))
}

错误转换

正如在讲解create handler 时所说,我们经常在 Rust 开发中使用map_err,它的作用是:将某个类型为S的错误,转换成类型为E的错误。

比如:

let client = state.pool.get().await.map_err(AppError::from)?;

从连接池获取客户端时,可能发生错误,这个错误是由 deadpool-postgres 定义的PoolError,我们需要对其进行转换:

let client = state.pool.get().await.map_err(|err| AppError{
    cause: Some(err.to_string()),
    // 其它字段省略
})?;

但我们的代码中,使用是 AppError::from。那是因为我们为 AppError 实现了 From trait:

impl From<deadpool_postgres::PoolError> for AppError {
    fn from(err: deadpool_postgres::PoolError) -> Self {
        Self::db_error(err)
    }
}
impl From<tokio_postgres::Error> for AppError {
    fn from(err: tokio_postgres::Error) -> Self {
        Self::db_error(err)
    }
}
  • 实现From<deadpool_postgres::PoolError>:可以直接在map_err中使用AppError::fromdeadpool_postgres::PoolError转换成AppError。在尝试从连接池中获取 Client 时,有可能出现这个错误。

  • 实现From<tokio_postgres::Error>:可以直接在map_err中使用AppError::fromtokio_postgres::Error转换成AppError。在数据库操作时(比如preparequeryexecuterollbackcommit)有可能出现这个错误。

虽然我们实现了 TodoList 的功能,但存在一些问题:

  • 重复代码太多

  • 未对错误进行日志记录

下一章我们将对代码进行重构,以及记录可能发生的错误。