现在是时候开始进行数据库操作,以便实现功能了。本章将实现TodoList
的功能。
本章代码在04/数据库连接及模型定义分支。
增加数据库配置
/// 应用配置
#[derive(Deserialize)]
pub struct Config {
pub web: WebConfig,
pub pg: deadpool_postgres::Config,
}
状态共享
为了在整个应用间共享数据库连接池,我们需要定义状态共享:
#[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
/// 待办列表模型
#[derive(PostgresMapper, Serialize)]
#[pg_mapper(table = "todo_list")]
pub struct TodoList {
pub id: i32,
pub title: String,
}
/// 待办列表新ID模型
#[derive(PostgresMapper, Serialize)]
#[pg_mapper(table = "todo_list")]
pub struct TodoListID {
pub id: i32,
}
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
模块。
下面以create
和find
来对 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::from
将deadpool_postgres::PoolError
转换成AppError
。在尝试从连接池中获取 Client 时,有可能出现这个错误。 -
实现
From<tokio_postgres::Error>
:可以直接在map_err
中使用AppError::from
将tokio_postgres::Error
转换成AppError
。在数据库操作时(比如prepare
、query
、execute
、rollback
、commit
)有可能出现这个错误。
虽然我们实现了 TodoList 的功能,但存在一些问题:
-
重复代码太多
-
未对错误进行日志记录
下一章我们将对代码进行重构,以及记录可能发生的错误。