使用 Diesel 和 Rust 进行高效数据库操作

856 阅读10分钟

大家好,我是梦兽。一个 WEB 全栈开发和 Rust 爱好者。如果你对 Rust 非常感兴趣,可以关注梦兽编程公众号获取群,进入和梦兽一起交流。

image.png image.png

图片是我们日常开发使用最多的一种开发方式,最后一般是指lambda_http或者Rust集合的操作。

在本文中,我们将探讨如何使用 Diesel 以异步方式操作关系型数据库。

具体来说,我们将涵盖以下内容:

  • 设置 Diesel
  • 使用 diesel_async 进行基本的数据操作(列出、获取、插入、更新、删除)
  • 使用 Axum 与 Lambda 集成
  • 我将在本例中使用 PostgreSQL!

「安装 Diesel CLI」

Diesel 提供了一个独立的命令行工具来帮助管理我们的项目。因为它是一个独立的二进制文件,我们需要在系统上安装它。

cargo install diesel_cli

如果你遇到像 note: ld: library not found for -xxx 这样的错误,这可能意味着你缺少用于数据库后端所需的客户端库。默认情况下,Diesel CLI 依赖于以下客户端库:

  • libpq 用于 PostgreSQL 后端
  • libmysqlclient 用于 MySQL 后端
  • libsqlite3 用于 SQLite 后端

既然我只使用 PostgreSQL,我也可以选择仅安装带有 PostgreSQL 支持的 diesel_cli

cargo install diesel_cli --no-default-features --features postgres

「设置」

「创建新项目」

首先,让我们通过 cargo new rust_diesel_demo 创建一个新的项目,并向我们的 Cargo.toml 文件中添加以下 [dependencies]

[dependencies]
bb8 = "0.8.0"
diesel = "2.0.3"
diesel-async = { version = "0.2.1", features = ["postgres""bb8"] }

这里是我们将要使用的三个 crates:

  • 「Diesel」: 用于在 Rust 中与数据库交互的基础 crate。
  • 「diesel-async」: Diesel 的扩展。请注意,它只提供了核心 Diesel 特性的异步变体。
  • 「bb8」: 一个全功能的连接池,专为异步连接设计。

请注意,我们将不会直接与 Diesel 交互,而是通过 diesel-async。

「设置 Diesel」

现在,我们需要告诉 Diesel 在哪里可以找到我们的数据库,方法是设置 DATABASE_URL 环境变量。这个变量可以全局设置。然而,因为我们可能同时有多个项目进行,所以建议将它放在项目目录下的 .env 文件中。

确保你处于项目的根目录下,并运行以下命令:

echo DATABASE_URL=postgres://username:password@localhost/diesel_demo > .env

我们现在可以使用 diesel setup 命令来进行设置。

这将创建我们的数据库(如果还不存在的话),并创建一个空的迁移目录,我们可以用它来管理数据库模式。还会创建一个名为 diesel.toml 的文件,该文件告诉 Diesel 为我们维护一个位于 src/schema.rs 的文件。

image.png

「迁移」

如上所述,迁移是我们用来管理数据库模式并允许我们随时间演进数据库模式的方法。我们已经在上面创建了一个空数据库,现在让我们向其中添加一个 users 表以便进行操作。

要为此创建一个迁移:

diesel migration generate create_users

这条命令将为我们创建以下两个空文件:

Creating migrations/2024-04-25-062901_create_users/up.sql
Creating migrations/2024-04-25-062901_create_users/down.sql

每个迁移都可以被应用(up.sql)或回滚(down.sql)。应用然后立即回滚一个迁移应该不会改变数据库的模式。

接下来,我们将添加迁移的 SQL 语句:

-- up

CREATE TABLE users (
  id VARCHAR PRIMARY KEY,
  username VARCHAR NOT NULL
)

-- down

DROP TABLE users

运行我们的迁移文件,我们可以这么做

diesel migration run

如果你需要回滚,这里建议先进行检查down.sql的可行性,避免不必要的redo过程。

这里的 "redo" 可能是指重新执行迁移的过程,即先应用迁移 (up.sql),然后回滚 (down.sql),以确保 down.sql 能够正确地撤销所做的更改。这样做是为了验证 down.sql 文件是否编写正确,能够将数据库恢复到迁移前的状态。

diesel migration redo

此外,如果你想回滚一个迁移,你可以运行以下命令:

diesel migration revert

redo 命令基本上是 runrevert 的组合。

迁移成功后,你也应该会自动生成一个 schema.rs 文件,内容类似于下面的样子。

// @generated automatically by Diesel CLI.

diesel::table! {
    users (id) {
        id -> Varchar,
        username -> Varchar,
    }
}

这个文件会在我们运行或回滚迁移的任何时候自动更新。

你可以使用自己选择的数据库可视化工具来确认数据库及其中的表是否已成功创建。

image.png

image.png

到了用 Rust 的时候了!

「定义 User 结构体」

为了能够与我们的 users 表进行交互,我们需要添加一个 User 结构体,其中的字段与表中的字段相同。

use diesel::prelude::*;
use serde::Serialize;

#[derive(Debug, Queryable, Selectable, Serialize)]
#[diesel(table_name = crate::schema::users)]
pub struct User {
    pub id: String,
    pub username: String,
}

让我们来看看这里使用的一些宏。

  • #[derive(Queryable)]: 生成所有从 SQL 查询加载 User 结构体所需的代码。
  • #[derive(Selectable)]: 生成基于模型类型定义的表 (#[diesel(table_name = crate::schema::users)]) 匹配的选择子句代码。

这里有一个重要的事情需要注意:

  • #[derive(Queryable)] 假定 User 结构体上的字段顺序与 users 表中的列顺序匹配,因此请确保按照 schema.rs 文件中看到的顺序定义它们。

注意,Serialize 只是因为我们计划稍后从 Lambda 函数返回 User 结构体作为 JSON 响应才需要的。

基本的数据库操作 连接池 为什么我们需要连接池?每次建立新的数据库连接都是低效的,并且在高流量的情况下可能导致资源耗尽。连接池维护了一组打开的数据库连接,以便于重复使用这些连接。

我们将使用 bb8,这是一个功能齐全的连接池,专为异步连接(使用 tokio)设计。

为了创建连接池:

pub async fn get_connection_pool(
    db_url: &str,
) -> Result<Pool<AsyncDieselConnectionManager<AsyncPgConnection>>, Error> {
    let config = AsyncDieselConnectionManager::<AsyncPgConnection>::new(db_url);
    let pool = Pool::builder().build(config).await?;
    Ok(pool)
}

然后,我们可以在每次需要时从连接池中获取连接:

let pool: Pool<AsyncDieselConnectionManager<AsyncPgConnection>> =
    get_connection_pool(&db_url).await?;
let mut conn: PooledConnection<'_AsyncDieselConnectionManager<AsyncPgConnection>> =
    pool.get().await?;

现在我们已经准备好了连接,让我们来看一下使用 Diesel 进行一些基本的数据操作。

「列出所有用户」

为了列出我们 users 表中的所有条目。

use schema::users::dsl::*;    
let results = users
    .load::<User>(&mut conn)
    .await?;
println!("Displaying {} users", results.len());
for user in results {
    println!("{}", user.id);
    println!("{}", user.username);
    println!("-----------\n");
}

这将返回一个 Vec<User> 类型的结果。

Displaying 1 users
f9d05aad-f05f-454b-b9c9-2735882153d2
itsuki
-----------

如果我们只需要单个字段,例如 username,该怎么办?为此,我们可以使用 select

let results = users
    .select(username)
    .load::<String>(&mut conn)
    .await?;

println!("Displaying {} users", results.len());
for name in results {
    println!("{}", name);
    println!("-----------\n");
}

这一次,我们的结果将是一个 Vec<String> 类型的列表。

Displaying 1 users
itsuki
-----------

如果我们只对 users 表的总数感兴趣,我们可以使用 execute。不同于 loadget_resultget_results(我们稍后会更详细地探讨这些方法),execute 只会返回受影响的行数。

「获取单个用户」

要根据 iduser_id 获取单个用户,我们可以使用 find,它会尝试根据主键从给定的表中查找单个记录。

use schema::users::dsl::*;
let user_id = "some_id"
let user = users
    .find(user_id)
    .select(User::as_select())
    .first(&mut conn)
    .await?;
println!("{:?}", user);

我们将会得到:

// success
User { id"some_id"username"itsuki" }
// failure
ErrorRecord not found

find 只适用于主键。如果我们想根据非主键值获取一行,我们将不得不使用 filter。我会稍后展示如何做到这一点。

「插入新用户」

为了将新用户插入到我们的数据库中,让我们首先创建一个 NewUser 结构体,我们将使用它来插入新记录。

#[derive(Deserialize, Insertable)]
#[diesel(table_name = crate::schema::users)]
pub struct NewUser {
    pub id: String,
    pub username: String,
}

在这种情况下,我们的 NewUserUser 实际上是相同的,因此从技术上讲,我们可以将它们合并为一个。但是,我认为保持它们分开是一个好的做法,以防我们在表中有某些默认字段,在创建新记录时不需要这些字段。

我将使用 uuid 生成 id,所以我们需要在 Cargo.toml 中添加以下依赖项。

uuid = {version = "1.8.0", features = ["v4""serde"]}

插入新记录非常直接,因为我们已经为我们衍生出了 Insertable

use schema::users;
let uuid = Uuid::new_v4().to_string();
    let new_user = NewUser {
        id: uuid,
        username: "itsuki".to_owned(),
    };

let user = diesel::insert_into(users::table)
    .values(&new_user)
    .returning(User::as_returning())
    .get_result(&mut conn)
    .await?;

println!("newuser created: {:?}", user);

请注意,当我们对插入或更新语句调用 .get_result 时,它会自动在查询末尾添加 RETURNING *,并让我们将其加载到任何实现了正确类型的 Queryable 的结构体中。

如果我们不需要返回结果怎么办?同样地,就像上面仅获取用户数量的情况一样,我们可以使用 execute 命令。

use schema::users;
let inserted_rows = diesel::insert_into(users::table)
    .values(&new_user)
    .execute(&mut conn)
    .await?;

「插入多个用户」

我们可以在单个查询中插入多条记录。只需将 Vec 或切片传递给 insert_into,然后调用 get_results 而不是 get_result


use schema::users;

let new_users = vec![
    NewUser {
        id: Uuid::new_v4().to_string(),
        username: "itsuki_mul_1".to_owned(),
    },
    NewUser {
        id: Uuid::new_v4().to_string(),
        username: "itsuki_mul_2".to_owned(),
    },
];

let users = diesel::insert_into(users::table)
  .values(&new_users)
  .returning(User::as_returning())
  .get_results(&mut conn)
  .await?;

「更新用户信息」

为了更新现有记录(行)的特定字段,我们可以使用 diesel::update

例如,为了更新具有特定 id 的用户的 username

use schema::users::dsl::{username, users};
let new_username = "itsuki";
let user = diesel::update(users.find(id))
    .set(username.eq(new_username))
    .returning(User::as_returning())
    .get_result(&mut conn)
    .await?;

请注意,如果我们不指定 users.find(id),表中的每一行都将被更新!(每一行都会变成 itsuki!)

我们也可以同时更新多个列。假设我们还有另一个列 nickname(在这个例子中我们没有)。

let user = diesel::update(users.find(id))
    .set((username.eq(new_username), nickname.eq(new_nickname)))
    .returning(User::as_returning())
    .get_result(&mut conn)
    .await?;

「删除用户」

为了根据主键的值删除,我们可以使用 find

use schema::users::dsl::*;

let num_deleted = diesel::delete(users.find(user_id))
    .execute(&mut conn)
    .await?;

我们也可以根据特定列的值进行删除。例如,如果我们想删除所有用户名以 itsuki 开头的用户。

use schema::users::dsl::*;

let num_deleted = diesel::delete(users.filter(username.like("itsuki%")))
    .execute(&mut conn)
    .await?;

完整的代码展示

pub async fn list_users(
    pool: &Pool<AsyncDieselConnectionManager<AsyncPgConnection>>,
) -> Result<Vec<User>> {
    use schema::users::dsl::*;

    // get connection
    let mut conn: PooledConnection<'_, AsyncDieselConnectionManager<AsyncPgConnection>> =
        pool.get().await?;

    let results = users.load::<User>(&mut conn).await?;

    Ok(results)
}

pub async fn get_user(
    pool: &Pool<AsyncDieselConnectionManager<AsyncPgConnection>>,
    user_id: &str,
) -> Result<User> {
    use schema::users::dsl::*;

    // get connection
    let mut conn: PooledConnection<'_, AsyncDieselConnectionManager<AsyncPgConnection>> =
        pool.get().await?;

    let user = users
        .find(user_id)
        .select(User::as_select())
        .first(&mut conn)
        .await?;

    Ok(user)
}

pub async fn create_single_user(
    pool: &Pool<AsyncDieselConnectionManager<AsyncPgConnection>>,
    new_user: NewUser,
) -> Result<User> {
    use schema::users;

    let mut conn: PooledConnection<'_, AsyncDieselConnectionManager<AsyncPgConnection>> =
        pool.get().await?;

    let user = diesel::insert_into(users::table)
        .values(&new_user)
        .returning(User::as_returning())
        .get_result(&mut conn)
        .await?;

    Ok(user)
}

pub async fn create_multiple_user(
    pool: &Pool<AsyncDieselConnectionManager<AsyncPgConnection>>,
    new_users: Vec<NewUser>,
) -> Result<Vec<User>> {
    use schema::users;

    let mut conn: PooledConnection<'_, AsyncDieselConnectionManager<AsyncPgConnection>> =
        pool.get().await?;

    let users = diesel::insert_into(users::table)
        .values(&new_users)
        .returning(User::as_returning())
        .get_results(&mut conn)
        .await?;

    Ok(users)
}

pub async fn update_username(
    pool: &Pool<AsyncDieselConnectionManager<AsyncPgConnection>>,
    id: &str,
    new_username: &str,
) -> Result<User> {
    use schema::users::dsl::{username, users};

    let mut conn: PooledConnection<'_, AsyncDieselConnectionManager<AsyncPgConnection>> =
        pool.get().await?;

    let user = diesel::update(users.find(id))
        .set(username.eq(new_username))
        .returning(User::as_returning())
        .get_result(&mut conn)
        .await?;

    Ok(user)
}

pub async fn delete_user(
    pool: &Pool<AsyncDieselConnectionManager<AsyncPgConnection>>,
    user_id: &str,
) -> Result<usize> {
    use schema::users::dsl::*;

    // get connection
    let mut conn: PooledConnection<'_, AsyncDieselConnectionManager<AsyncPgConnection>> =
        pool.get().await?;

    let num_deleted = diesel::delete(users.find(user_id))
        .execute(&mut conn)
        .await?;

    Ok(num_deleted)
}

结束语

感谢阅读!感谢您的时间,并希望您觉得这篇文章有价值。在您的下一个 JavaScript 项目中尝试使用柯里化,并在下面的评论中告诉我它如何改善了您的编码体验!

创建和维护这个博客以及相关的库带来了十分庞大的工作量,即便我十分热爱它们,仍然需要你们的支持。或者转发文章。通过赞助我,可以让我有能投入更多时间与精力在创造新内容,开发新功能上。赞助我最好的办法是微信公众号看看广告。

本文使用 markdown.com.cn 排版