创建数据库访问层

未匹配的标注

创建数据库访问层

为了方便上层应用程序,我们将在 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 层函数,并使用一个子命令来调用它。

(译者注:以下为多余部分,我也不知为何会多,提交的原文本来没有的)
创建数据库访问层

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~