创建数据库访问层
创建数据库访问层#
为了方便上层应用程序,我们将在 diesel 代码周围编写一个 精简的 wrappers 集,并将其作为模块公开。
作为测试这些 wrappers 的一种方法,我们将使用每个 wrapper 函数的子命令来构建可执行文件。然后,这组子命令可以用作 调试 / 故障排除 / 实验 的工具。
插入一个任务#
我们的数据库现在令人难受,因为现在只有一张表,并且里面没有一行数据。让我们来解决这个问题 —— 首先,这儿有两个给你的小惊喜。
你可能已经注意到了在你的目录下有 diesel.toml
文件:
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
这是我们运行的时候生成的,注意它指向的是 src/schema.rs
。让我们来看一下内容:
table! {
task (id) {
id -> Integer,
title -> Text,
}
}
运行我们的 schema migration 的时候,会自动获得该文件 —— 每次运行 migration (不管是 up 还是 down)时它将自动更新,你所看到的 table!
宏会生成一堆供我们在处理数据库中的表时使用的代码。
接下来,我们需要一点胶水(glue)。在开始敲击键盘之前,我们需要考虑将代码保留在何处。现在,我们所拥有的是一个二进制 crate ,但我们想要创建一个小库,计划有如下模块结构:
mytodo
+-- db
| +-- models
| +-- schema
+-- rest
现在,我们将创建 db 模块,其余的以后创建。我们得让 Rust 知道我们正在通过创建src/lib.rs
来创建 db 模块:
#[macro_use]
extern crate diesel;
pub mod db;
这也引入了 Diesel 的宏 —— 我们的代码将会严重依赖这些宏。
并且我们想要 Diesel 将 schema.rs 指定一个新位置,所以让我们更改 diesel.toml
:
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/db/schema.rs"
我们可以通过移除已有的 schema.rs
并重新运行 migration 以生成一个新文件来测试是否正确设置了 diesel :
$ rm src/schema.rs
$ diesel migration redo
Rolling back migration 2019-08-19-023055_task
Running migration 2019-08-19-023055_task
$ ls src/db/
schema.rs
我所提到的那一点胶水就是 上面的树中所显示的 models
模块。在这里面我们会定义一些可用于读写数据库的类型。并且 现在 我们已经准备好写一些代码了,创建 src/db/models.rs
:
use super::schema::task;
#[derive(Insertable)]
#[table_name = "task"]
pub struct NewTask<'a> {
pub title: &'a str,
}
通过 Insertable 派生我们的结构,并设置 table_name
为我们的 schema 中的内容, Diesel 会自动地提供执行数据库插入的代码 —— 在 src/db/mod.rs
我们可以添加函数以利用这一点:
use diesel::{prelude::*, sqlite::SqliteConnection};
pub mod models;
pub mod schema;
pub fn establish_connection() -> SqliteConnection {
let db = "./testdb.sqlite3";
SqliteConnection::establish(db)
.unwrap_or_else(|_| panic!("Error connecting to {}", db))
}
pub fn create_task(connection: &SqliteConnection, title: &str) {
let task = models::NewTask { title };
diesel::insert_into(schema::task::table)
.values(&task)
.execute(connection)
.expect("Error inserting new task");
}
第一行展示了 Diesel 中我们所需要的,接下来两行展示了我们的 models 和 schema 子模块。然后有一个便利的函数用以为我们数据库创建一个 SqliteConnection
。(注:相比我们正在构建的这个玩具级别的应用,更需要一种更好的机制来设置数据库的路径。)
最后, create_task
函数是非常简单的,我们从 models 里声明的结构中创建一个对象。 Diesel 的 insert_into
从 schema 中获取表, 返回给我们一个可以通过 values
添加的对象,这又返回一个可以execute
的对象。
在实际的应用程序中,我们可以更好地处理错误。还要注意,我们故意丢弃了返回值,它将是一个 usize
,表示所创建的行数。其它数据库引擎(例如 Postgres)可以让刚刚插入的 id 行在查询结果中可用,但是 SQLite 不能做到这一点。
我们可以通过 cargo build
来确保所有东西都写对了。希望看到一些关于未使用函数的 dead_code
警告 —— 我们将会通过使用它们来解决这个问题(译者注:也可以更改 dead_code
lint 的级别,不过没必要。)。但首先我们需要一个钩子 (hook) 来 勾连 (hang)
这个用法。
创建开发工具#
前面我提到过一个可以用来读写数据库的工具,现在我们将为此而创建该基础结构。首先,让我们创建一个新的二进制文件:
$ mkdir src/bin/
现在是 src/bin/todo.rs
,一步一步来,首先我们需要 std::env
处理命令行参数,并且我们需要刚编写的 db 函数。
use std::env;
use mytodo::db::{create_task, establish_connection};
然后我们得有一个打印帮助的函数,以给用户提供一些无法解析的参数集,
fn help() {
println!("subcommands:");
println!(" new<title>: create a new task");
}
在我们的主函数种,我们只是将第一个参数与可能的子命令集匹配,并将剩下的参数分配给处理程序 —— 并在没有参数或我们无法理解它的情况下调用 help
。
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
help();
return;
}
let subcommand = &args[1];
match subcommand.as_ref() {
"new" => new_task(&args[2..]),
_ => help(),
}
}
最后,我们有了子命令处理程序,该命令打开一个数据库连接,并将 title 向下传递到 db 层。
fn new_task(args: &[String]) {
if args.len() < 1 {
println!("new: missing <title>");
help();
return;
}
let conn = establish_connection();
create_task(&conn, &args[0]);
}
现在我们可以对其进行测试了!
$ cargo run --bin todo new 'do the thing'
Compiling mytodo v0.1.0 (/projects/rust/mytodo)
Finished dev [unoptimized + debuginfo] target(s) in 1.47s
Running `target/debug/todo new 'do the thing'`
$ cargo run --bin todo new 'get stuff done'
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/todo new 'get stuff done'`
$ echo 'select * from task;' | sqlite3 testdb.sqlite3
1|do the thing
2|get stuff done
注意最后一个命令 —— 如果你在这儿有一些 SQL,你应该在弄脏之前进行清洗(go wash up before it stains ,译者注:也就是恢复原来状态。),当你往下看的时候我们会提供一种更为安全的办法来查询任务。
查询任务#
编写插入函数的时候,我们使用了从 Insertable
派生的结构。这样或许你就不会对我们要使用从 Queryable
派生的结构体感到惊讶了。将其添加到 src/db/models.rs
:
#[derive(Queryable)]
pub struct Task {
pub id: i32,
pub title: String,
}
值得注意的是 —— 因为其是如此吸引人 —— 你不可能从可查询和可插入的结构中同时派生一个结构。当你在执行插入操作时这将要求你设置 id
,并且我们总是想要让数据库引擎来自动分配 id 。
在 src/db/mod.rs
中,我们可以添加一个函数,在查询 task 表的时候返回一个新的 model 的 Vec :
pub fn query_task(connection: &SqliteConnection) -> Vec<models::Task> {
schema::task::table
.load::<models::Task>(connection)
.expect("Error loading tasks")
}
添加新的子命令处理程序到 src/bin/todo.rs
:
fn show_tasks(args: &[String]) {
if args.len() > 0 {
println!("show: unexpected argument");
help();
return;
}
let conn = establish_connection();
println!("TASKS\n-----");
for task in query_task(&conn) {
println!("{}", task.title);
}
}
对于读者来说,在 main
中添加 match 和帮助信息输出行是一个简单的练习。
现在,我们可以测试我们的 query 函数和 insertion 函数,而无需任何 SQL。
$ cargo run --bin todo show
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/todo show`
TASKS
-----
do the thing
get stuff done
数据库层总结#
我们已经编写了一个非常简单(即,绝对最小)数据抽象层,并且通过编写一个用于查询和查看数据库的 CLI 工具来进行练习。
我们的数据模型是如此简单以至于应用实际上还不可用(没有标记完成任务(task)的机制,甚至没有删除任务的机制)。下面的练习可以解决这个问题。
我们完全忽略了:
- 注释
- 文档 (除
help
之外) - 测试
- 持续集成
还有其他的用于维护质量这类的东西。在生产级应用程序中,我们肯定会添加这些内容。
然而,就算有这些缺点,我们也有足够的基础结构来构建我们的下一个层 —— REST API 。尝试一下这些练习,我们将在 REST API 一章中试验。
数据库层练习#
在表格中添加 “done” 列#
编写和运行一个 migration,更新 models,更新插入代码以在创建时将 task 设置为待处理( not done ),并更新 show 子命令以显示 完成 / 待完成状态。
添加一个子命令来标记 task 完成,你需要考虑是否让用户提供 title 或 id 。
如果是前者,在 db 层,你可能需要添加一个按 title 查找的函数,并且从子命令调用它以便于将 id 传给另一个新的 db 层函数,你将添加设置 done 为 true 的更新记录。
如果是后者,你可能需要修改 show 子命令来显示 id 。
这听起来像是有很多步骤,但他们都非常简单。查阅 Diesel 指南 获取有关 filter
查询和 update
操作的帮助。
添加子命令来删除 task#
如上所述,你需要决定是让用户提供 id 还是 title 。 然后添加一个删除 task 的 db 层函数,并使用一个子命令来调用它。
(译者注:以下为多余部分,我也不知为何会多,提交的原文本来没有的)
创建数据库访问层
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。