在本指南中,我们将通过一系列简单的示例来逐一介绍CRUD操作,即“创建、读取、更新、删除”。本指南的每一步都是在前一步的基础上进行的,建议按顺序跟随操作。

在开始之前,请确保您已经安装并运行了PostgreSQL数据库。如果您使用的是其他类型的数据库,比如SQLite,部分示例可能无法直接应用,因为不同数据库的API实现可能会有所差异。在项目仓库中,您可以找到适用于每个支持数据库的各种示例

Diesel 要求使用 Rust 1.65 或更新版本。如果您正在按照本指南学习,请确保您通过执行 rustup update stable 命令,升级到至少 Rust 1.65 版本。

本文使用的 diesel 版本为 2.1.4, 最新信息请参考官方网站

创建新项目

我们首先做的事创建一个新项目.

创建新项目
cargo new --lib diesel_demo
cd diesel_demo

首先,我们需要将Diesel添加到项目的依赖中。此外,我们还将使用一个名为 .env 的工具来管理我们的环境变量。我们也会把它加入到项目的依赖中。

Cargo.toml
[dependencies]
diesel = { version = "2.1.0", features = ["postgres"] }
dotenvy = "0.15"

安装 Diesel CLI

Diesel提供了一个独立的命令行工具来辅助项目管理。这个工具是一个独立的可执行文件,不会直接影响项目的代码,因此我们不需要将其添加到 Cargo.toml 文件中。我们只需在系统中安装这个工具即可。

安装 diesel_cli
cargo install diesel_cli
安装 `diesel_cli` 的注意事项:

如果您遇到类似这样的错误:

note: ld: library not found for -lmysqlclient
clang: error: linker command failed with exit code 1 (use -v > to see invocation)

这表明您缺少了某个数据库后端所需的客户端库——在此例中是 mysqlclient。您可以通过安装相应的库(根据您的操作系统选择常规安装方法)或者使用 --no-default-features 标志来排除不需要的默认库来解决这个问题。 diesel CLI 默认依赖于以下客户端库:

如果您不确定如何安装这些依赖,可以查阅相关依赖的文档或使用您的操作系统包管理器。

例如,如果您只安装了 PostgreSQL,可以通过以下命令只安装支持 PostgreSQL 的 diesel_cli:

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

如果您不确定如何配置这些依赖,可以查看 diesel 的持续集成(CI) 配置,了解不同操作系统的有效配置示例。

配置 Diesel

我们需要告知 Diesel 我们的数据库位置。这可以通过设置 DATABASE_URL 环境变量来实现。在开发机器上,我们可能同时处理多个项目,不希望环境变量变得混乱。因此,我们可以将数据库的 URL 放在一个 .env 文件中,以保持运行环境的清洁。

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

现在,Diesel CLI可以帮我们完成所有设置工作。

diesel setup

这将会创建我们的数据库(如果它尚未存在的话),并且创建一个空的 migrations 目录,我们可以用它来管理我们的数据库结构(稍后将详细介绍)。

现在我们将编写一个小型的命令行工具(CLI),用它来管理一个博客(暂且不考虑我们只能通过这个CLI访问数据库的问题)。我们首先需要的是一个数据库表来存放我们的博文。让我们为此创建一个数据库 migration

diesel migration generate create_posts

Diesel CLI 会为我们创建两个必要的空文件。您将看到如下类似的输出:

Creating migrations/20160815133237_create_posts/up.sql
Creating migrations/20160815133237_create_posts/down.sql

migration 功能让我们能够随着时间的推移逐步修改数据库结构。每个 migration 都可以被实施(up.sql)或撤销(down.sql)。实施一个 migration 后立即撤销它,数据库结构应该会保持原样。

下一步,我们将编写 migration 所需的SQL代码:

up.sql
CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title VARCHAR NOT NULL,
  body TEXT NOT NULL,
  published BOOLEAN NOT NULL DEFAULT FALSE
)
down.sql
DROP TABLE posts

现在可以应用我们创建的 migration:

diesel migration run

确保 down.sql 文件的正确性是一个好习惯。您可以通过重新应用 migration 来快速验证 down.sql 是否正确地撤销了 migration 操作:

diesel migration redo
关于迁移中使用原始SQL的注意事项:

由于迁移是以纯SQL编写的,它们可以利用您所用数据库系统的特定功能。例如,上述 CREATE TABLE 语句就用到了 PostgreSQL 的 SERIAL 数据类型。如果您想改用 SQLite,则需要使用 INTEGER 类型。diesel的GitHub仓库 提供了适用于所有支持后端的修改示例。如果您使用的不是PostgreSQL,请务必查看这些示例。
如果您更愿意通过Rust代码来生成迁移脚本,diesel CLI工具为diesel migration generate 命令提供了一个额外的 `--diff-schema`` 选项,它可以根据当前的架构定义和您的数据库生成迁移脚本。要生成与前面展示的原始SQL 迁移等效的迁移脚本,您需要执行以下步骤:

  • 创建一个名为 schema.rs 的文件,并填入以下内容:
diesel::table! {
   posts (id) {
       id -> Int4,
       title -> Varchar,
       body -> Text,
       published -> Bool,
   }
}


table! 宏的文档中包含了该宏的语法规则。diesel::sql_types 模块提供了定义相关列时使用的SQL类型的相关文档。

  • 执行 diesel migration generate --diff-schema create_posts

这将会生成迁移的 up.sqldown.sql 文件,并预先填充了必要的SQL语句。完成这一步后,您应该继续执行 diesel migration run 命令。

关于在生产环境中使用迁移的注意事项:

在将应用程序部署到生产环境之前,您可能需要在其初始化阶段执行数据库迁移。您还可能希望将迁移脚本整合到代码中,这样可以避免将脚本复制到部署的服务器或容器中。

diesel_migrations crate 提供了一个 embed_migrations! 宏,它可以让您将迁移脚本直接嵌入到最终生成的二进制文件中。在您的代码中使用了这个宏之后,只需在 main 函数的起始部分加入 connection.run_pending_migrations(MIGRATIONS),这样每次应用程序启动时都会自动执行迁移。

编写 Rust 代码

好了,SQL的部分就到这里,现在让我们开始编写一些Rust代码。我们将从实现显示最近发布的五篇帖子的功能开始。首先,我们需要建立一个数据库连接。

src/lib.rs
use diesel::pg::PgConnection;
use diesel::prelude::*;
use dotenvy::dotenv;
use std::env;

pub fn establish_connection() -> PgConnection {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    PgConnection::establish(&database_url)
        .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}

我们还需要创建一个 Post 结构体,用来存储我们的数据,并且让 diesel 为我们在查询中引用的表和列生成相应的名称。

我们将在 src/lib.rs 文件的顶部加入以下代码行:

src/lib.rs
pub mod models;
pub mod schema;

接下来我们需要添加刚刚声明的两个 mod.

src/models.rs
use diesel::prelude::*;

#[derive(Queryable, Selectable)]
#[diesel(table_name = crate::schema::posts)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub body: String,
    pub published: bool,
}

#[derive(Queryable)] 将生成从 SQL 查询中加载 Post 结构体所需的所有代码。#[derive(Selectable)] 将通过 #[diesel(table_name = crate::schema::posts)] 基于定义的表生成构造匹配 select 子句的代码。#[diesel(check_for_backend(diesel::pg::Pg))] 是一个可选的注解,它增加了编译时的检查,以确保您的结构体中的所有字段类型都与它们对应的 SQL 表达式兼容,从而大幅提升编译器错误信息的可读性。

通常情况下,schema 模块不是手动编写的,而是由 diesel CLI 工具生成的。在我们运行 diesel setup 命令时,会创建一个名为 diesel.toml 的文件,该文件指示 Diesel 自动维护 src/schema.rs 文件。这个文件的内容大致如下:

src/schema.rs
// @generated automatically by Diesel CLI.

diesel::table! {
    posts (id) {
        id -> Int4,
        title -> Varchar,
        body -> Text,
        published -> Bool,
    }
}

具体的输出可能会根据您使用的数据库略有差异,但它们的基本作用是相同的。

table! 宏根据数据库结构生成了一系列代码,用以表示所有的表格和列。我们将在接下来的示例中详细了解如何使用这些代码。如果您想深入了解生成的代码,可以参考 深入 Diesel Schema

每次我们执行或撤销迁移时,这个文件都会自动同步更新。

关于字段顺序的注意事项

使用 #[derive(Queryable)] 时,它会假设 Post 结构体中的字段顺序与 posts 表中的列顺序一致,因此请确保按照 schema.rs 文件中的顺序来定义这些字段。同时使用 #[derive(Selectable)]SelectableHelper::as_select 可以保证字段顺序始终一致。

现在,我们来编写代码以展示我们的博文。

src/bin/show_posts.rs
use self::models::*;
use diesel::prelude::*;
use diesel_demo::*;

fn main() {
    use self::schema::posts::dsl::*;

    let connection = &mut establish_connection();
    let results = posts
        .filter(published.eq(true))
        .limit(5)
        .select(Post::as_select())
        .load(connection)
        .expect("Error loading posts");

    println!("Displaying {} posts", results.len());
    for post in results {
        println!("{}", post.title);
        println!("-----------\n");
        println!("{}", post.body);
    }
}

通过使用 self::schema::posts::dsl::* 这行代码,我们导入了一系列别名,这样我们就可以直接使用 posts 而不需要 posts::table,以及 published 而不是 posts::published。当我们的操作只涉及一个表格时,这种方式非常有用。但是,我们并不总是需要这样做。重要的是,我们应该将 schema::table::dsl::* 的导入限制在当前函数中,以避免污染模块的命名空间。

我们可以通过执行 cargo run --bin show_posts 来运行我们的脚本。不过,由于数据库中还没有任何帖子,所以结果可能不会太吸引人。尽管如此,我们已经编写了大量的代码,现在是时候进行提交了。

目前这个阶段的完整代码可以在这个链接中找到。

接下来,我们将编写代码以创建一篇新的帖子。为此,我们需要一个结构体来插入新的记录。

src/models.rs
use crate::schema::posts;

#[derive(Insertable)]
#[diesel(table_name = posts)]
pub struct NewPost<'a> {
    pub title: &'a str,
    pub body: &'a str,
}

现在让我们添加一个函数来保存一篇新帖子。

src/lib.rs
use self::models::{NewPost, Post};

pub fn create_post(conn: &mut PgConnection, title: &str, body: &str) -> Post {
    use crate::schema::posts;

    let new_post = NewPost { title, body };

    diesel::insert_into(posts::table)
        .values(&new_post)
        .returning(Post::as_returning())
        .get_result(conn)
        .expect("Error saving new post")
}

当我们对一个插入或更新语句调用 .get_result 时,它会自动在查询末尾加上 RETURNING *,并且允许我们将查询结果加载到任何实现了 Queryable 并且类型匹配的结构体中。非常棒!

关于 RETURNING 子句的注意事项

并非所有的数据库都支持 RETURNING 子句。在一些支持 RETURNING 子句的数据库(例如 PostgreSQL 和 SQLite)上,我们可以在插入操作中获取数据。在 SQLite 后端,RETURNING 子句从 3.35.0 版本开始得到支持。为了启用 RETURNING 子句,您需要添加一个特性标志,如 returning_clauses_for_sqlite_3_35。MySQL 不支持 RETURNING 子句。为了获取所有插入的行,我们可以调用 .get_results 而不是 ``.execute`。如果您在不同的数据库系统上按照这个指南操作,请务必查看适用于您数据库系统的具体示例

Diesel 允许您在一次查询中插入多条记录。只需将一个 Vec 或 slice 传递给 insert_into 方法,然后调用 get_results 而不是 get_result。如果您实际上不想处理刚插入的行,可以选择调用 .execute 方法。这样编译器就不会报错了。:)

现在我们已经完成了所有设置,我们可以编写一个小脚本来创建一篇新帖子。

src/bin/write_post.rs
use diesel_demo::*;
use std::io::{stdin, Read};

fn main() {
    let connection = &mut establish_connection();

    let mut title = String::new();
    let mut body = String::new();

    println!("What would you like your title to be?");
    stdin().read_line(&mut title).unwrap();
    let title = title.trim_end(); // Remove the trailing newline

    println!(
        "\nOk! Let's write {} (Press {} when finished)\n",
        title, EOF
    );
    stdin().read_to_string(&mut body).unwrap();

    let post = create_post(connection, title, &body);
    println!("\nSaved draft {} with id {}", title, post.id);
}

#[cfg(not(windows))]
const EOF: &str = "CTRL+D";

#[cfg(windows)]
const EOF: &str = "CTRL+Z";

我们可以使用 cargo run --bin write_post 命令来运行我们新编写的脚本。去写一篇博文吧!发挥你的创造力!我的博文是这样的:

   Compiling diesel_demo v0.1.0 (file:///Users/sean/Documents/Projects/open-source/diesel_demo)
     Running `target/debug/write_post`

What would you like your title to be?
Diesel demo

Ok! Let's write Diesel demo (Press CTRL+D when finished)

You know, a CLI application probably isn't the best interface for a blog demo.
But really I just wanted a semi-simple example, where I could focus on Diesel.
I didn't want to get bogged down in some web framework here.

Saved draft Diesel demo with id 1

不幸的是,运行 show_posts 仍然无法显示我们刚刚保存的新帖子,因为我们是作为草稿保存的。如果我们回顾一下 show_posts 中的代码,我们会发现我们添加了 .filter(published.eq(true)),并且在我们的迁移脚本中 published 默认是 false。我们需要发布它!但为了做到这一点,我们首先需要了解如何更新现有的记录。首先,让我们进行代码提交。此时此地的完整代码示例可以在这里找到。

既然创建和读取的部分已经完成,更新操作实际上就相对简单了。让我们直接进入脚本的编写:

src/bin/publish_post.rs
use self::models::Post;
use diesel::prelude::*;
use diesel_demo::*;
use std::env::args;

fn main() {
    use self::schema::posts::dsl::{posts, published};

    let id = args()
        .nth(1)
        .expect("publish_post requires a post id")
        .parse::<i32>()
        .expect("Invalid ID");
    let connection = &mut establish_connection();

    let post = diesel::update(posts.find(id))
        .set(published.eq(true))
        .returning(Post::as_returning())
        .get_result(connection)
        .unwrap();
    println!("Published post {}", post.title);
}

这就完成了!让我们使用 cargo run --bin publish_post 1 来试一试。

 Compiling diesel_demo v0.1.0 (file:///Users/sean/Documents/Projects/open-source/diesel_demo)
   Running `target/debug/publish_post 1`
Published post Diesel demo

接下来,我们还将实现一个功能,用于获取单个帖子。我们会显示帖子的ID及其标题。请注意 .optional() 调用。这将返回一个 Option<Post> 类型的结果,而不是抛出错误,之后我们可以在匹配模式中使用这个返回值。如果您想要了解如何修改构造的 select 语句,请查阅 QueryDsl 的文档

src/bin/get_post.rs
use self::models::Post;
use diesel::prelude::*;
use diesel_demo::*;
use std::env::args;

fn main() {
    use self::schema::posts::dsl::posts;

    let post_id = args()
        .nth(1)
        .expect("get_post requires a post id")
        .parse::<i32>()
        .expect("Invalid ID");

    let connection = &mut establish_connection();

    let post = posts
        .find(post_id)
        .select(Post::as_select())
        .first(connection)
        .optional(); // This allows for returning an Option<Post>, otherwise it will throw an error

    match post {
        Ok(Some(post)) => println!("Post with id: {} has a title: {}", post.id, post.title),
        Ok(None) => println!("Unable to find post {}", post_id),
        Err(_) => println!("An error occured while fetching post {}", post_id),
    }
}

我们可以运行 cargo run --bin get_post 1 来查看我们的博文。

 Compiling diesel_demo v0.1.0 (file:///Users/sean/Documents/Projects/open-source/diesel_demo)
   Running `target/debug/get_post 1`
   Post with id: 1 has a title: Diesel demo

最后,我们可以运行 cargo run --bin show_posts 来查看我们的博文。

     Running `target/debug/show_posts`
Displaying 1 posts
Diesel demo
----------

You know, a CLI application probably isn't the best interface for a blog demo.
But really I just wanted a semi-simple example, where I could focus on Diesel.
I didn't want to get bogged down in some web framework here.
Plus I don't really like the Rust web frameworks out there. We might make a
new one, soon.

我们只介绍了 CRUD 操作中的三个步骤。现在,让我们展示如何删除数据。有时候我们会写一些我们真的很不喜欢的内容,而没有时间去查找其 ID。因此,我们可以根据标题,甚至是标题中的某些词来删除这些内容。

src/bin/delete_post.rs
use diesel::prelude::*;
use diesel_demo::*;
use std::env::args;

fn main() {
    use self::schema::posts::dsl::*;

    let target = args().nth(1).expect("Expected a target to match against");
    let pattern = format!("%{}%", target);

    let connection = &mut establish_connection();
    let num_deleted = diesel::delete(posts.filter(title.like(pattern)))
        .execute(connection)
        .expect("Error deleting posts");

    println!("Deleted {} posts", num_deleted);
}

我们可以运行 cargo run --bin delete_post demo 来执行脚本(至少适用于我的文章标题)。您的输出应该类似于:

   Compiling diesel_demo v0.1.0 (file:///Users/sean/Documents/Projects/open-source/diesel_demo)
     Running `target/debug/delete_post demo`
Deleted 1 posts

当我们再次尝试执行 cargo run --bin show_posts 命令时,我们可以看到帖子确实已经被删除了。虽然这仅仅展示了 Diesel 功能的一小部分,但希望这个教程为您奠定了坚实的基础。我们建议您进一步探索 API 文档,以了解更多内容。本教程的完整代码可以在以下链接中找到。