Diesel 入门指南
在本指南中,我们将通过一系列简单的示例来逐一介绍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 的工具来管理我们的环境变量。我们也会把它加入到项目的依赖中。
[dependencies]
diesel = { version = "2.1.0", features = ["postgres"] }
dotenvy = "0.15"
安装 Diesel CLI
Diesel提供了一个独立的命令行工具来辅助项目管理。这个工具是一个独立的可执行文件,不会直接影响项目的代码,因此我们不需要将其添加到 Cargo.toml
文件中。我们只需在系统中安装这个工具即可。
cargo install 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 后端的 libpq
- MySQL 后端的 libmysqlclient
- SQLite 后端的 libsqlite3
如果您不确定如何安装这些依赖,可以查阅相关依赖的文档或使用您的操作系统包管理器。
例如,如果您只安装了 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代码:
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
body TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE
)
DROP TABLE posts
现在可以应用我们创建的 migration:
diesel migration run
确保 down.sql
文件的正确性是一个好习惯。您可以通过重新应用 migration 来快速验证 down.sql
是否正确地撤销了 migration 操作:
diesel migration redo
由于迁移是以纯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.sql
和down.sql
文件,并预先填充了必要的SQL语句。完成这一步后,您应该继续执行diesel migration run
命令。
在将应用程序部署到生产环境之前,您可能需要在其初始化阶段执行数据库迁移。您还可能希望将迁移脚本整合到代码中,这样可以避免将脚本复制到部署的服务器或容器中。
diesel_migrations crate 提供了一个 embed_migrations! 宏,它可以让您将迁移脚本直接嵌入到最终生成的二进制文件中。在您的代码中使用了这个宏之后,只需在
main
函数的起始部分加入connection.run_pending_migrations(MIGRATIONS)
,这样每次应用程序启动时都会自动执行迁移。
编写 Rust 代码
好了,SQL的部分就到这里,现在让我们开始编写一些Rust代码。我们将从实现显示最近发布的五篇帖子的功能开始。首先,我们需要建立一个数据库连接。
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
文件的顶部加入以下代码行:
pub mod models;
pub mod schema;
接下来我们需要添加刚刚声明的两个 mod.
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 文件。这个文件的内容大致如下:
// @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 可以保证字段顺序始终一致。
现在,我们来编写代码以展示我们的博文。
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
来运行我们的脚本。不过,由于数据库中还没有任何帖子,所以结果可能不会太吸引人。尽管如此,我们已经编写了大量的代码,现在是时候进行提交了。
目前这个阶段的完整代码可以在这个链接中找到。
接下来,我们将编写代码以创建一篇新的帖子。为此,我们需要一个结构体来插入新的记录。
use crate::schema::posts;
#[derive(Insertable)]
#[diesel(table_name = posts)]
pub struct NewPost<'a> {
pub title: &'a str,
pub body: &'a str,
}
现在让我们添加一个函数来保存一篇新帖子。
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 子句的数据库(例如 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 方法。这样编译器就不会报错了。:)
现在我们已经完成了所有设置,我们可以编写一个小脚本来创建一篇新帖子。
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。我们需要发布它!但为了做到这一点,我们首先需要了解如何更新现有的记录。首先,让我们进行代码提交。此时此地的完整代码示例可以在这里找到。
既然创建和读取的部分已经完成,更新操作实际上就相对简单了。让我们直接进入脚本的编写:
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 的文档。
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。因此,我们可以根据标题,甚至是标题中的某些词来删除这些内容。
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 文档,以了解更多内容。本教程的完整代码可以在以下链接中找到。