裂化柴油

裂化柴油

原文:https://medium.com/hackernoon/grokking-diesel-652cb8886a63

Diesel ( http://diesel.rs )是用 Rust 编写的 ORM(对象关系映射器)和查询生成器。它支持 Postgresql、Mysql 和 SQLite。它利用 Rust 的自定义派生功能来生成您需要的所有代码,以便在与数据库交互时获得 Rust 类型系统的所有功能。这意味着您获得了代码的编译时验证,从而消除了可能的运行时错误。

Diesel 的功能非常强大,但是仅仅按照例子来做可能还是会让你挠头,问一些问题,比如“这些类型是从哪里来的?”、“我应该在这里添加什么”、“这是如何工作的?”。本文旨在从一个中级 Rust 开发人员的角度,对这个主题进行一点阐释。

有许多主题,这篇文章将不会涵盖,但希望您将能够“grok”足够的柴油,以便其余的变得显而易见,并且您理解它是如何工作的,在某种程度上,您在这个机箱中探索的新领域也变得更容易理解。

第一步

你应该先浏览一下 Diesel 官方网站上的入门指南。本文主要关注 Postgresql,到目前为止,让 Postgresql 服务器为您的测试运行的最简单的方法是使用 Docker(www.docker.com)...下面的一行程序应该可以解决这个问题:

docker run --rm --detach --name postgres --env POSTGRES_USER=username --env POSTGRES_PASSWORD=password --publish 127.0.0.1:5432:5432 postgres

这是而不是用于生产,当容器停止时,整个容器将被移除(使用 *docker stop postgres* ),但它对于入门指南来说非常方便。

关于本文其余部分的格式

本文假设您已经阅读了入门指南;它故意掩盖了一整套特征。这是有意的,这样做是为了让文章尽可能简洁。

核心组件和社区

其核心柴油由 4 个主要成分组成:

到目前为止,您应该已经了解了diesel crate、diesel_codegen crate(代码生成器)和diesel cli。

最后一个主要组件,即“秘密武器”或“非官方指南”(据我所知),是官方回购中的测试套件(https://github . com/diesel-RS/diesel/tree/master/diesel _ tests)。每当我陷入困境时,它就充当我的向导,尽管随着项目的继续发展,它肯定会被文档所取代。

环境

当我开始使用 Diesel 时,我被schema.rs中的infer_schema!宏弄糊涂了(下面会详细介绍),它反过来使用了"dotenv:DATABASE_URL"。这个dotenv箱子只是一个方便的库,允许你把你的DATABASE_URL放在一个隐藏的环境文件中,ala .env (dot env,明白吗?).

接下来,你也可以在你的环境中指定DATABASE_URL,当你在一个 docker 容器 ala 12 因子应用中运行你的使用 Diesel 的 Rust 二进制时,这特别方便。

Diesel cli 工具还读取.env文件,如果该文件不可用,您需要在环境中定义DATABASE_URL或者将--database-url参数传递给它。

一定要查看 *examples/sqlite/getting_started_step_3/README.md* 文件,以便了解如何为 SQLite 配置 *DATABASE_URL* (它不使用 URI 格式)。

基本流程

创建使用 Diesel 构建的应用程序的核心工作流程可以分解如下:

设计一个模式

对于经验丰富的 SQL 老手来说,这是非常明显的,幸运的是,对于我们其余的人来说,diesel cli 的migration子命令允许我们轻松地迭代我们的设计,甚至随着时间的推移不断发展。然而,预先清楚地知道您希望数据库是什么样子是非常有用的,应该注意的是,Diesel 只适用于有主键的表。

创建迁移

遵循《入门指南》中的模式,注意您可以随时使用diesel migration generate子命令添加更多的迁移。每当您运行diesel migration run时,迁移将按顺序运行。您可以通过发出diesel migration redo命令重新运行上一次迁移,如果您确实遇到了问题,您可以运行下面的命令,但是请不要在生产数据库上这样做,diesel database reset

设计表格时,你应该使用复数形式的表格名称。例如,Diesel 将采用模型User并搜索表users。您可以定义自定义的表名,但是了解 Diesel 开发人员的假设可能会避免一些混乱。

Diesel 将获取PascalCaseRust struct(它可能描述单个对象,或者数据库表中的行),并将它们翻译成snake_case表名,并在末尾添加一个s,使其“多元化”。例如,AFancyNamedObject将被假设映射到一个名为a_fancy_named_objects的表。

从数据库中推断模式

Diesel 能够检查您的实际数据库,并推断出 Rust 中使用的模式,这将用于创建必要的 DSL(领域特定语言),允许您以安全、快速和强类型的方式与数据库进行交互。

入门指南和示例使用了infer_schema!宏。使用这个宏的主要缺点是,首先,你不知道 Diesel 实际上是如何解释你的数据库类型的(这对你的模型很重要,这将在后面揭示),其次,它在编译时需要一个自举的数据库实例,当作为管道的一部分进行编译时,可能会有点痛苦,因为你的数据库和编译器工具链不能互相使用。

建议您使用diesel print-schema子命令,简单地将推断的模式复制并粘贴到项目中的一个文件中(在《入门指南》中,这是schema.rs,但也可以很容易地粘贴到lib.rsmain.rs)。

您将看到一个类似于以下内容的table!宏:

table! {
    users {
        id -> Integer,
        name -> VarChar,
    }
}

您想要获取数据类型(在本例中是IntegerVarChar)并立即运行到docs.rs/diesel并将其插入到文档搜索栏中。这将直接带您到diesel::types::Foo(也探索diesel::types),并允许您检查每种类型已实现的ToSqlFromSql特征。举个例子,Integer映射到 Rust 中的i32。这在实现模型或处理编译时错误时非常有用。

创建模型

与模式一样,您不需要将您的模型放在models.rs文件中。建议您使用 Rust modules 系统来拆分您的模型,类似下面的内容可能会有所帮助,尤其是在处理大量模型时。

models/
  users/
    mod.rs
  posts/
    mod.rs

我最初挣扎着把用来驱动柴油的“魔法”从惯用的铁锈中分裂出来。原来,一旦你知道了它是如何构造的,以及现在如何处理生成的代码,Diesel 就只是一个熟悉的老东西。

下面是一个用 Rust struct 表示的示例模型,直接摘自入门指南:

#[derive(Queryable)]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub body: String,
    pub published: bool,
}

模型是普通的 Rust 结构,似乎映射到你的表。这通常是简单模型的情况,然而,注意到Queryable结构,顾名思义,实际上映射到您想要从 SQL 查询中获得的结果是非常重要的。

Queryable struct 是您希望从 SQL 查询中检索的单个对象或行(部分或完整)。这个User对象可以从users表中查询,或者任何返回正确类型的查询,如下面的Queryable部分所讨论的。

请注意,diesel 的 SQL 类型不支持 Postgresql 中的无符号整数(这是 Postgresql 的一个限制),因此查看文档中适当的diesel::*::types来了解您的数据库支持什么是值得的。

衍生,又名“密码魔术”

Diesel 的代码生成器主要用于使用 SQL magic 嵌入您的 Rust 结构,而无需您手动编写大量功能。它还将 Rust 类型的系统变成了神奇的成分,可以用来构建快速可靠的 SQL,并传输到数据库。

为了让您的结构具有 SQL 的优点,您可以使用 Rust 的自定义派生功能,例如:

#[derive(Queryable)]
pub struct Post {
    pub id: i32,
    ...

一个更复杂的例子:

我喜欢在测试中偶然发现以下内容……这确实让我对作者的天才一笑置之,但也让我挠头,“这些东西到底用在哪里,它们是如何工作的?”

#[derive(PartialEq, Eq, Debug, Clone, Queryable, Identifiable, Insertable, AsChangeset, Associations)]
#[table_name = "users"]
pub struct User {
    pub id: i32,
    pub name: String,
    pub hair_color: Option<String>,
}

首先,新手给你一个“专业建议”,你应该安装 cargo 子命令expand:

cargo install cargo-expand

这允许您运行下面的命令,并且实际上看到生成的内容(警告,本指南假设您了解 Rust,但是下面的内容可能会让即使是经验丰富的 Rust 开发人员也热泪盈眶,因此可以随意跳过它或用作睡前读物)。

cargo expand

所生成的 DSL 的全部优点显露出来了…特别有趣的是每个模型的列。解析输出是无聊的读者或喜欢挑战的读者的一项练习。

让我们更详细地看一下推导过程。

PartialEq,Eq,调试,克隆

这些是大多数开发人员在适当的时候依赖的标准 Rust 派生。如果你喜欢使用println!(或者闪亮的新eprintln!)进行调试,你可能至少需要Debug

*#![deny(missing_debug_implementations)]* 是一个非常有用的编译器指令,因为它会在任何没有 *Debug* 实现的结构上出错,如果您正在开发一个库,这尤其有用。

可查询的

大多数开发人员通常想对数据库做的第一件事是查询其中的数据。为了做到这一点,你需要将你的struct装饰如下:

#[derive(Queryable)]
struct User {
    id: i32,
    firstname: String,
    lastname: String,
    age: i32,
}

这将导致diesel_codegen生成反序列化类型为(i32, String, String, i32)的查询结果所需的查询 DSL,该查询结果映射到 Postgresql 中的(Integer, Text, Text, Integer),更多示例见文章末尾。

在 SQLite 中,日期/时间类型也可以反序列化为 *String*

使用 Diesel 时,主键列是必需的,但无论如何,这可能是一个好的实践。

如何使用 DSL 来构造查询将在本文后面讨论。

可插入的

除非您来自现有的系统,否则您可能也想将数据插入到数据库中。典型的“可插入”对象如下所示:

#[derive(Insertable)]
#[table_name="users"]
struct NewUser {
    firstname: String,
    lastname: String,
    age: i32,
}

请注意,我们已经删除了id字段,因为 SQL server 将为我们处理这个问题(对于高级用例,这可能会有所变化)。

还要注意,我们现在显式地命名该表为users,这对于AsChangeset也是需要的,并且对于Identifiable是推断出来的

可辨认的

不可避免地,您将使用 SQL 连接一次从多个表中构造结果。为了使连接成功地解析目标表中的精确对象,需要对这个struct进行如下注释:

struct User {
    id: i32,
    firstname: String,
    lastname: String,
    age: i32,
}

默认情况下,Diesel 将假设您的主键名为id,如果不是,您可以按如下方式覆盖它:

#[derive(Identifiable)]
#[primary_key(guid)]
struct User {
    guid: i32,
    firstname: String,
    lastname: String,
    age: i32,
}

联合

需要用您的Identifiable数据充实的表格需要注释如下:

#[derive(Associations)]
#[belongs_to(User)]
struct ActiveUsers {
    id: i32,
    user_id: i32,
    last_active: NaiveDateTime
}

这将允许您通过查找与User表中的id字段相对应的user_id字段定义的用户,将User数据加入到该表中。如果您的外键字段没有在模式type_id中指定,您需要手动映射它:

#[derive(Associations)]
#[belongs_to(User, foreign_key="user_lookup_key")]
struct ActiveUsers {
    id: i32,
    user_lookup_key: i32,
    last_active: NaiveDateTime,
}

AsChangeSet

#[derive(AsChangeset)]用于更新,更多详情可在本进行中指南中找到:https://github . com/diesel-RS/diesel/blob/master/guide _ drafts/all-about-updates . MD

使用柴油

此时,您已经准备好用 Diesel 做一些事情,可能会插入一些数据并进行一些查询。《入门指南》中有多个示例介绍了这一点。然而,有些东西需要拆开包装,希望能让灯泡继续工作。

首先,在这一点上,强调你正在进入正常的生锈世界是非常重要的。我这么说的意思是,Diesel 的代码和魔法部分现在退居二线了。

这意味着当您查看示例代码时,不会有更多的疏忽。你只需要相信编译器,像平常一样花时间解开语句和表达式。你会发现你正在调用普通的方法,传递普通的数据结构(或者对它们的引用),你可以像对待其他 Rust 代码一样println!、调试、步进、重新排序和组织。不幸的是,我花了很长时间才理解这一点,但这是一个重要的见解,可以让你无所畏惧地编码。

连接到数据库

postgresql示例使用函数pub fn establish_connection() -> PgConnection将连接包装成一个方便的函数调用,以便在示例代码的其余部分重用。

PgConnection为你封装了posgresql的句柄,保留这个对象或者按需重新创建它。需要时,查看r2d2创建一个数据库连接池。

使用 Diesel 执行的所有操作都取决于可用的连接对象。

插入数据

本指南在src/lib.rs中指定了以下功能。让我们一步一步来:

pub fn create_post(conn: &PgConnection, title: &str, body: &str) -> Post {
    use schema::posts;
    let new_post = NewPost {
        title: title,
        body: body,
    }; diesel::insert(&new_post).into(posts::table)
        .get_result(conn)
        .expect("Error saving new post")
}

首先注意连接&PgConnection的引用。正如对src/bin/中的其他一些文件所做的那样,一个新的PgConnection也可以通过调用如下内容来实例化:

let conn = establish_connection();

下一行,use schema::posts;,困扰了我很长时间,因为这是使用生成的代码。在diesel::insert表达式中,我们看到了posts::table的用法。

此时,获取cargo expand的输出并在某种 IDE 中查看它可能是个好主意。尝试以下方法:

# Check out a copy of Diesel     git clone [https://github.com/diesel-rs/diesel.git](https://github.com/diesel-rs/diesel.git)
cd diesel/examples/postgres/getting_started_step_3/# Build the example (assuming your postgres instance is ready
# and running; see the docker hint above)echo DATABASE_URL=postgres://username:password@localhost/diesel_demo > .env
diesel setup
cargo build# Expand the code (assuming cargo-expand is installed)cargo expand --lib > expanded.rs

如果您在输出下搜索mod schema,您还会看到mod posts,在那里您会找到一个table结构(空的,但是有一堆特征被导入)。

接下来我们有一个新的物体(T4),叫做 T5。

最后我们有实际的柴油方法insert。如果您查阅文档,您会看到 Diesel 模块有 5 个功能,其核心是insertdeleteinsert_default_valuesselectupdate

如果您查看insert函数,您会看到它接受records: T(在本例中是我们的新用户,但也可以是Vec<T>)并返回一个IncompleteInsertStatement对象,该对象有一个名为into()的方法,该方法接受您的table结构。您也可以调用into(::schema::posts::table)并避免使用语句。

在这种情况下,schema的名称,也就是您的模块的名称,因为模式是在schema.rs文件中生成的。

查询数据

同样,我们将在指南中引用一个函数,在本例中是src/bin/show_posts.rs文件:

extern crate diesel_demo;
extern crate diesel;
use self::diesel_demo::*;
use self::diesel_demo::models::*;
use self::diesel::prelude::*;fn main() { use diesel_demo::schema::posts::dsl::*;
    let connection = establish_connection();
    let results = posts.filter(published.eq(true))
        .limit(5)
        .load::<Post>(&connection)
        .expect("Error loading posts"); println!("Displaying {} posts", results.len()); for post in results {
        println!("{}", post.title);
        println!("----------\n");
        println!("{}", post.body);
    }
}

从头开始,首先我们导入自己的板条箱(示例Cargo.tomlcrate名称指定为diesel_demo)。我们进口柴油箱。

use self::diesel_demo::*;让我们访问establish_connection()函数,use self::diesel_demo::models::*;让我们访问我们在models.rs文件中定义的实际结构,在本例中是Post结构。

use self::diesel::prelude::*;是一个必要的导入,将一大堆柴油特性和类型纳入范围。这是 Diesel 工作所必需的,超出了本文的讨论范围。

main()函数中,我们再次遇到了一些魔法的使用,特别是use diesel_demo::schema::posts::dsl::*;行。当我第一次开始使用 Diesel 时,我被上面插入代码时使用的schema::tablename::*导入和这里使用的schema::posts::dsl::* dsl 导入的区别难住了。

看一下cargo expand的输出,可以得到一些澄清:

pub mod dsl {
    pub use super::columns::*;
    pub use super::table as posts;
}

简而言之,schema::posts::dsl::*将您的表的columns带入范围。为您生成的每个列类型都有一个在其上实现的expression_methods集合。换句话说,dsl(领域特定语言)允许我们在表名中使用columns(由schema.rs生成的代码定义)并对其应用逻辑,以便构造我们的 SQL 查询。(见http://docs . diesel . RS/diesel/expression methods/global expression _ methods/trait。expression methods . html # method . desc

如果您发现了将table方便地导入到 dsl 模块中(我认为这是为了方便),那么您将获得额外的加分。

构建查询

在 SQL 中,我们构造一个 select 表达式来从我们的数据库中返回值,在 Diesel 中,我们使用 Rust 的类型系统来构造类型检查的、安全的版本。这对于任何曾经与 SQL 查询的脆弱性及其隐含的安全风险作斗争的人来说都是非常好的…只有有效的 SQL 才能成功编译。

下一个表达式posts.filter(published.eq(true))反映了我们想要在posts表上运行filter方法(也可以通过use schema::posts::dsl::*语句方便地导入到我们的上下文中)。filter将构建的过滤器作为其输入。通过组合列及其表达式方法来构造过滤器。

要检查结果,您可以将相关代码重写为:

use diesel::debug_sql;let posts_with_sql = posts.filter(published.eq(true))
    .limit(5);println!("SelectStatement: {:#?}", posts_with_sql);let results = posts_with_sql
    .load::<Post>(&connection)
    .expect("Error loading posts");

如果您查看终端的输出,您会看到返回了一个SelectStatement对象。您还可以使用println!("SQL: {}", debug_sql!(posts_with_sql));查看将要生成的 SQL(使用#[macro_use] extern crate diesel;导入 diesel)。

本节的主要内容是,首先使用从dsl模块导入的tablecolumns构建相关的 SQL 查询,这可以通过println!debug_sql进行自省。

您应该尽可能地熟悉您可以在列上调用的不同的expression_methods——它将帮助您顺利地构建您想要在应用程序中使用的 SQL 查询。

获得结果

本文中我们要处理的最后一项实际上是读取您的结果。在show_posts.rs二进制中,这是通过以下代码行实现的:

let results = ...omitted...
    .load::<Post>(&connection)
    .expect("Error loading posts");

可以看到,我们正在调用我们前面检查过的SelectStatement结构的.load()方法。.load()方法是通用的,所以我们需要给编译器一些提示,告诉它我们想返回什么类型。加载函数的参数是对由establish_connection返回的connection对象的引用(或借用)。

load返回一个结果对象,在这个可执行文件中,我们通过用expect()方法展开结果来处理它。然后,如果成功,我们将结果传递给results绑定。

参见文档中的diesel::prelude::LoadDsldiesel::prelude::FirstDsl了解load的一些替代方案。正如将要看到的,这些方法返回一个QueryResult,它只是一个Result<T, Error>;类型的别名。

在刚才提到的所有方法中,要么返回单个对象(QueryResult<T>),要么返回对象的向量(QueryResult<Vec<T>>)。换句话说,数据库表中所选列的一行或多行。

与结果角力

如果我们想得到所有的结果,我们可以使用代码:

let results :Vec<Post> = posts
    .load(&connection)
    .expect("Error loading posts");

loadget_results是等价的。也就是说,它们都返回一个Vec<T>结果(QueryResult)。注意,代码被重新格式化,以说明在绑定let results: Vec<Post> = ...中给编译器的返回类型提示。

最后,有时你会使用一个select方法或join方法(本文中没有举例说明,但是请查看上面提到的‘非官方指南’又名diesel_tests),它们会返回不直接映射到你的模型中的字段的行。在这种情况下,使用tuple从你的load方法中收集你的结果。这可能看起来像:

let results: Vec<(i32, String, String, bool)>= posts
    .load(&connection)
    .expect("Error loading posts");

上面,我们将Post型号或struct表示为其组成部件的tuple

我们没有涵盖的内容以及下一步要做什么

本文中没有涉及到很多内容,尽管就其本身而言,这是一个相当大的信息量。如前所述,目标是解释足够多的 Diesel 及其结构,以便感兴趣的开发人员可以“搜索”或理解它,然后自助。

我希望你能走到这一步,并享受这段旅程,请发送反馈并享受 Rust 和 Diesel。

特别感谢《Diesel》的作者 Sean Griffin,他对本文进行了事实核查。


本站为非盈利网站,作品由网友提供上传,如无意中有侵犯您的版权,请联系删除