用 Rust Actix-web 写一个 Todo 应用(三)── migrations 和错误处理
使用 diesel 管理数据库变化
diesel 是一个用 rust 写的 ORM 库,支持多种数据库,同时 diesel 提供了对数据库结构的管理功能。我们将使用 diesel 对我们的数据库结构变化进行管理。
首先,安装命令行工具 diesel_cli
,并初始化数据库设置
# 安装 diesel_cli,支持 postgres
cargo install diesel_cli --no-default-features --features postgres
# 设置数据库连接
echo DATABASE_URL=postgres://actix:actix@localhost:5432/actix >> .env
# 生成 diesel.toml 文件指向 schema 所在
diesel setup
# 创建数据库 migration
diesel migration generate create_db
在生成的 migrations
目录中,填入数据库变化的 sql 语句,up.sql
用于修改,down.sql
用于撤销修改。
up.sql
:
create table todo_list (
id serial primary key,
title varchar(150) not null
);
create table todo_item (
id serial primary key,
title varchar(150) not null,
checked boolean not null default false,
list_id integer not null,
foreign key (list_id) references todo_list(id)
);
down.sql
:
drop table if exists todo_item;
drop table if exists todo_list;
由于之前已经有对应的数据表结构,需要将原来的表结构删除,再运行数据库变更:
# 删除原有的数据表之后
diesel migrations run
其中,对应生成的 schema 为:
#![allow(unused)] fn main() { table! { todo_item (id) { id -> Int4, title -> Varchar, checked -> Bool, list_id -> Int4, } } table! { todo_list (id) { id -> Int4, title -> Varchar, } } joinable!(todo_item -> todo_list (list_id)); allow_tables_to_appear_in_same_query!( todo_item, todo_list, ); }
此时,数据库中的表机构就和我们之前是一样的,同时增加了一个用于记录已经做过的 migrations 的数据库。
ORM
鉴于 diesel 没有 async 版本,以及 quaint 不是 type-safe,不做 ORM 的支持。
错误处理
自定义错误,并将常见的错误统一处理。
新增 errors.rs
:
#![allow(unused)] fn main() { use actix_web::http::StatusCode; use actix_web::{HttpResponse, ResponseError}; use deadpool_postgres::PoolError; use serde::{Deserialize, Serialize}; use std::fmt; #[derive(Debug)] #[allow(dead_code)] pub enum Error { InternalServerError(String), NotFound(String), PoolError(String), } #[derive(Debug, Serialize, Deserialize)] pub struct ErrorResponse { errors: Vec<String>, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self) } } impl ResponseError for Error { fn error_response(&self) -> HttpResponse { match self { Error::NotFound(message) => { HttpResponse::NotFound().json::<ErrorResponse>(message.into()) } _ => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), } } } // 支持 字符串 into impl From<&String> for ErrorResponse { fn from(error: &String) -> Self { ErrorResponse { errors: vec![error.into()], } } } // 处理 PoolError impl From<PoolError> for Error { fn from(error: PoolError) -> Self { Error::PoolError(error.to_string()) } } }
修改 db.rs
:
#![allow(unused)] fn main() { +use crate::errors::Error; // ... - None => Err(Error::new(ErrorKind::NotFound, "Not found")), + None => Err(Error::NotFound("Not found".into())), }
修改 handlers.rs
,其中一个请求处理
#![allow(unused)] fn main() { +use crate::errors::Error; // error_response:items from traits can only be used if the trait is in scope +use actix_web::ResponseError; // ... -pub async fn todos(state: web::Data<AppState>) -> impl Responder { +pub async fn todos(state: web::Data<AppState>) -> Result<HttpResponse, Error> { let client: Client = state .pool .get() @@ -33,12 +34,15 @@ pub async fn todos(state: web::Data<AppState>) -> impl Responder { .expect("Error connecting to the database"); let result = db::get_todos(&client).await; match result { - Ok(todos) => HttpResponse::Ok().json(todos), - Err(_) => HttpResponse::InternalServerError().into(), + Ok(todos) => Ok(HttpResponse::Ok().json(todos)), + Err(e) => Ok(e.error_response()), } } }
小结
- 管理数据库结构变更;
- 自定义错误处理
参考文档和项目
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI