域名 AXUM.RS 将于2025年10月到期。我们无意再对其进行续费,我们希望你能够接续这个域名,让更多 AXUM 开发者继续受益。
  • 方案1️⃣AXUM.RS 域名 = 3000
  • 方案2️⃣方案1️⃣ + 本站所有专题原始 Markdown 文档 = 5000
  • 方案3️⃣方案2️⃣ + 本站原始数据库 = 5500
如果你有意接续这份 AXUM 情怀,请与我们取得联系。
说明:
  1. 如果有人购买 AXUM.RS 域名(方案1️⃣),或者该域名到期,本站将启用新的免费域名继续提供服务。
  2. 如果有人购买了 AXUM.RS 域名,且同时购买了内容和/或数据库(方案2️⃣/方案3️⃣),本站将关闭。届时我们或许会以另一种方式与你再相遇。

基本CRUD

本章我们将讨论使用 sqlx 和 PostgreSQL 执行基本的 CRUD (增删改查)操作。

示例数据

我们以一个非常简单的“分类”表来演示基本的 CRUD 操作,SQL如下:

CREATE TABLE IF NOT EXISTS "categories" (
    "id" SERIAL PRIMARY KEY,
    "name" VARCHAR(255) NOT NULL
);

模型定义

  • 为了让 sqlx 自动将数据库记录映射为 rust 结构体,我们需要使用 sqlx::FromRow

增加数据

数据库操作如下:

// src/category/model.rs

pub async fn insert(p: &PgPool, m: &Category) -> Result<i32> {
    let (id,): (i32,) =
        sqlx::query_as(r#"INSERT INTO categories ("name") VALUES ($1) RETURNING id"#)
            .bind(&m.name)
            .fetch_one(p)
            .await?;
    Ok(id)
}
  • PostgreSQL 支持 RETURNING 子句。本例中的 INSERT 语句会把新插入的 id 值返回

  • 为了获取到新插入的 id,我们配合 query_asfetch_one,有关这两个方法的说明,请参见下一章

  • bind():用于绑定参数。注意,PostgreSQL 的参数占位符是 $n,其中 n 是从 1 开始的编号,表示第n个参数

  • Result 是我们自定义的类型,其定义如下:

    // src/err.rs
    use axum::response::IntoResponse;
    
    #[derive(Debug)]
    pub struct Error(anyhow::Error);
    
    impl Error {
        pub fn new(msg: &str) -> Self {
            Self(anyhow::anyhow!("{}", msg))
        }
    }
    
    impl std::fmt::Display for Error {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{:?}", self.0)
        }
    }
    
    impl<E> From<E> for Error
    where
        E: Into<anyhow::Error>,
    {
        fn from(e: E) -> Self {
            Self(e.into())
        }
    }
    
    impl IntoResponse for Error {
        fn into_response(self) -> axum::response::Response {
            self.0.to_string().into_response()
        }
    }
    
    // src/lib.rs
    mod err;
    pub use err::Error;
    pub type Result<T> = std::result::Result<T, crate::Error>;
    

handler如下:

// src/category/handler.rs

#[derive(Deserialize)]
pub struct CreateForm {
    pub name: String,
}

pub async fn create(
    State(state): State<ArcAppState>,
    Json(frm): Json<CreateForm>,
) -> Result<Json<i32>> {
    let id = model::insert(
        &state.pool,
        &model::Category {
            name: frm.name,
            ..Default::default()
        },
    )
    .await?;
    Ok(Json(id))
}
  • 我们使用了 State():用于在 axum 应用中共享数据。其中的 ArcAppState 是我们定义的共享状态:

    // src/lib.rs
    pub struct AppState {
        pub pool: sqlx::PgPool,
    }
    
    pub type ArcAppState = std::sync::Arc<AppState>;
    

修改数据

数据库操作如下:

// src/category/model.rs

pub async fn update(p: &PgPool, m: &Category) -> Result<u64> {
    let aff = sqlx::query(r#"UPDATE categories SET "name" = $1 WHERE id = $2"#)
        .bind(&m.name)
        .bind(&m.id)
        .execute(p)
        .await?
        .rows_affected();
    Ok(aff)
}

handler如下:

// src/category/handler.rs

#[derive(Deserialize)]
pub struct EditForm {
    pub id: i32,
    pub name: String,
}

pub async fn edit(
    State(state): State<ArcAppState>,
    Json(frm): Json<EditForm>,
) -> Result<Json<u64>> {
    let aff = model::update(
        &state.pool,
        &model::Category {
            id: frm.id,
            name: frm.name,
        },
    )
    .await?;
    Ok(Json(aff))
}

删除数据

// src/category/model.rs

pub async fn delete(p: &PgPool, id: i32) -> Result<u64> {
    let aff = sqlx::query("DELETE FROM categories WHERE id = $1")
        .bind(id)
        .execute(p)
        .await?
        .rows_affected();
    Ok(aff)
}

对应的handler:

// src/category/handler.rs

pub async fn delete(State(state): State<ArcAppState>, Path(id): Path<i32>) -> Result<Json<u64>> {
    let aff = model::delete(&state.pool, id).await?;
    Ok(Json(aff))
}

查询单条记录

// src/category/model.rs

pub async fn find(p: &PgPool, id: i32) -> Result<Option<Category>> {
    let m = sqlx::query_as(r#"SELECT id, "name" FROM categories WHERE id = $1"#)
        .bind(id)
        .fetch_optional(p)
        .await?;
    Ok(m)
}

对应的handler:

// src/category/handler.rs

pub async fn find(
    State(state): State<ArcAppState>,
    Path(id): Path<i32>,
) -> Result<Json<Option<model::Category>>> {
    let m = model::find(&state.pool, id).await?;
    Ok(Json(m))
}

查询多条记录

对应的handler:

// src/main.rs

fn router_init(state: ArcAppState) -> Router {
    let category_router = Router::new()
        .route(
            "/",
            get(category::handler::list)
                .post(category::handler::create)
                .put(category::handler::edit),
        )
        .route(
            "/{id}",
            get(category::handler::find).delete(category::handler::delete),
        );

    Router::new()
        .nest("/category", category_router)
        .with_state(state)
}
  • axum v0.8 有一个重大改动:动态路由的参数由原来的 /:参数名 改为了 /{参数名},如本例的 route("/{id}", ...)

测试

我们使用 VSCODE 的 REST Client 插件来测试 HTTP 服务

本章代码位于01.crud分支。

要查看完整内容,请先登录