写点什么

当 diesel 遇见 jsonb

作者:伍思默
  • 2022 年 6 月 01 日
  • 本文字数:6387 字

    阅读完需:约 21 分钟

当 diesel 遇见 jsonb

如何使用 diesel 来操作 jsonb 类型的数据呢?

diesel cli 和 diesel migration

首先,我们需要安装 diesel:


cargo install diesel_cli
复制代码


其次,我们运行 diesel migration generate 命令来创建数据库表。


我们用一个名为 contacts 的表来存储联系人信息,它包含一个 jsonb 类型的字段,我们将在这个字段中存储联系人的信息:


diesel migration generate contacts
复制代码


以上命令会在 migrations 目录下生成一个新的文件,这个文件的名字是以时间戳命名的,我们可以查看这个文件:


exa -l --tree migrations
复制代码


输出:


drwxr-xr-x   - dudu 29 May 15:12 migrations/2022-05-29-000142_contacts.rw-r--r--  31 dudu 29 May 08:07 ├── down.sql.rw-r--r-- 293 dudu 29 May 15:12 └── up.sql
复制代码


下面我们来撰写 up.sql 文件,该文件用于创建数据库的表:


CREATE TABLE contacts (  id          BIGSERIAL,  name        VARCHAR(64) NOT NULL,  address     JSONB       DEFAULT NULL,  create_at   TIMESTAMPTZ NOT NULL DEFAULT now(),  update_at   TIMESTAMPTZ NOT NULL DEFAULT now(),  removed     BOOLEAN     NOT NULL DEFAULT FALSE,  PRIMARY KEY (id));
复制代码


我们也可以在 down.sql 文件中删除表:


DROP TABLE contacts;
复制代码


这样在下次运行 diesel migration revert 命令时,数据库表将被删除。


编辑好 up.sql 之后,我们可以运行 diesel migration run 命令来创建表:


diesel migration run
复制代码


查看下数据库的更改(表被创建出来了):


dudu@/tmp:t> \x off; \d contacts;Expanded display is off.+-----------+--------------------------+--------------------------------------------------------+| Column    | Type                     | Modifiers                                              ||-----------+--------------------------+--------------------------------------------------------|| id        | bigint                   |  not null default nextval('contacts_id_seq'::regclass) || name      | character varying(64)    |  not null                                              || address   | jsonb                    |                                                        || create_at | timestamp with time zone |  not null default now()                                || update_at | timestamp with time zone |  not null default now()                                || removed   | boolean                  |  not null default false                                |+-----------+--------------------------+--------------------------------------------------------+Indexes:    "contacts_pkey" PRIMARY KEY, btree (id)
Time: 0.010s
复制代码

diesel query

下面我们来向数据库写入数据。


有了数据的表,我们如何使用 diesel 把数据写入到数据库中呢?

model

首先定义 model:


use crate::schema::contacts;use chrono::Utc;
// schema:// table! {// contacts (id) {// id -> Int8,// name -> Varchar,// address -> Nullable<Jsonb>,// create_time -> Timestamptz,// update_time -> Timestamptz,// removed -> Bool,// }// }
#[derive(Queryable, QueryableByName, PartialEq, Debug)]#[table_name = "contacts"]pub struct Contact { pub id: i64, pub name: String, pub address: Option<serde_json::Value>, pub create_at: chrono::DateTime<Utc>, pub update_at: chrono::DateTime<Utc>, pub removed: bool,}
#[derive(AsChangeset, Insertable, Debug)]#[table_name = "contacts"]pub struct NewContact { pub id: i64, pub name: String, pub address: Option<serde_json::Value>, pub create_at: chrono::DateTime<Utc>, pub update_at: chrono::DateTime<Utc>, pub removed: bool,}
复制代码


保存内容到 src/model/contact.rs,并在 lib.rs 中导出:


#[macro_use]extern crate diesel;extern crate dotenv;extern crate r2d2;extern crate r2d2_diesel;
pub mod dao;pub mod db;pub mod model;pub mod schema;
复制代码


注意,diesel migration run 会生成或更新 schema 文件,我们可以查看 src/schema.rs:


table! {    contacts (id) {        id -> Int8,        name -> Varchar,        address -> Nullable<Jsonb>,        create_at -> Timestamptz,        update_at -> Timestamptz,        removed -> Bool,    }}
复制代码

dao

有了 model 之后,我们定义一个 dao,用于操作数据库:



use crate::model::contact::{Contact, NewContact};use diesel::pg::upsert::excluded;use diesel::prelude::*;
/// create batch contractspub fn create_contracts( conn: &PgConnection, new_contacts: &Vec<NewContact>,) -> QueryResult<Vec<Contact>> { use crate::schema::contacts::dsl::{address, contacts, name};
diesel::insert_into(contacts) .values(new_contacts) .get_results(conn)}
复制代码


我们简单添加了一个 create_contracts 函数,用于插入数据。

insert

有了 dao 之后,我们可以插入数据:


use diesel::prelude::*;use diesel_example::dao::contact::create_contracts;use diesel_example::{    db,    model::contact::{Contact, NewContact},};use std::env;
const LOCAL_DATABASE_URL: &str = "postgres://localhost/t";
fn main() { let database_url = env::var("DATABASE_URL").unwrap_or(LOCAL_DATABASE_URL.into()); let pool = db::init_pool(database_url); let connection = pool.get().unwrap();
let conn: &PgConnection = &connection; conn.execute("TRUNCATE TABLE contacts").unwrap(); conn.execute("alter sequence contacts_id_seq restart;").unwrap();
let santas_address: serde_json::Value = serde_json::from_str( r#"{ "street": "Article Circle Expressway 1", "city": "North Pole", "postcode": "99705", "state": "Alaska" }"#, ) .unwrap();
let my_address: serde_json::Value = serde_json::from_str( r#"{ "street": "Article Circle Expressway 2", "city": "North Pole", "postcode": "99705", "state": "Alaska" }"#, ) .unwrap();
let now = chrono::Utc::now();
let new_contacts = vec![ NewContact { id: 1, name: "Santa Claus".into(), address: Some(santas_address), create_at: now, update_at: now, removed: false, }, NewContact { id: 2, name: "dudu your neighbor".into(), address: Some(my_address), create_at: now, update_at: now, removed: false, }, ];
let contacts = create_contracts(&conn, &new_contacts).unwrap(); println!("{:?}", contacts);}
复制代码


以上做了这么几个工作:


  • 通过 db::init_pool 创建连接池,并从连接池中取回数据库连接

  • 通过 conn.execute("TRUNCATE TABLE contacts") 清空数据库中的数据

  • 通过 conn.execute("alter sequence contacts_id_seq restart;") 重置 id 的序列

  • 通过我们自己编写的 dao::create_contracts 函数插入数据,其中:

  • new_contacts 是一个 Vec<NewContact>,它包含了要插入的数据,此时我们准备了两条数据

  • 第一条数据是 Santa Claus,地址是 Article Circle Expressway 1

  • 第二条数据是 Dudu Your Neighbor,地址是 Article Circle Expressway 2


查看下写入了数据库(写入了圣诞老人和我家自己的地址):


dudu@/tmp:t> \x on; select * from contacts;Expanded display is on.-[ RECORD 1 ]-------------------------id        | 1name      | Santa Clausaddress   | {"city": "North Pole", "state": "Alaska", "street": "Article Circle Expressway 1", "postcode": "99705"}create_at | 2022-05-30 08:28:04.744632+08update_at | 2022-05-30 08:28:04.744632+08removed   | False-[ RECORD 2 ]-------------------------id        | 2name      | Dudu Your Neighboraddress   | {"city": "North Pole", "state": "Alaska", "street": "Article Circle Expressway 2", "postcode": "99705"}create_at | 2022-05-30 08:28:04.744632+08update_at | 2022-05-30 08:28:04.744632+08reSELECT 2Time: 0.001s
复制代码


这里列一下 db::init_pool 的代码:



use diesel::pg::PgConnection;
use r2d2;use r2d2_diesel::ConnectionManager;use std::ops::Deref;
pub type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
pub fn init_pool(db_url: String) -> Pool { let manager = ConnectionManager::<PgConnection>::new(db_url); r2d2::Pool::new(manager).expect("db pool failure")}
pub struct Conn(pub r2d2::PooledConnection<ConnectionManager<PgConnection>>);
impl Deref for Conn { type Target = PgConnection;
#[inline(always)] fn deref(&self) -> &Self::Target { &self.0 }}
复制代码


它使用 r2d2::Pool 创建连接池,从连接池中能够取回对 PostgreSQL 数据库的连接。

Query

Query in psql

我们可以用 postgres 的查询语法进行查询,比如:


首先进入 psql 命令行,这样我们就可以在命令行模式下进行查询了。


按 street (街道)在 address 中查询 (street = Article Circle Expressway 2):


dudu@/tmp:t> SELECT * FROM contacts WHERE address @> '{"street": "Article Circle Expressway 2"}';-[ RECORD 1 ]-------------------------id        | 2name      | dudu your neighboraddress   | {"city": "North Pole", "state": "Alaska", "street": "Article Circle Expressway 2", "postcode": "99705"}create_at | 2022-05-30 10:19:39.179315+08update_at | 2022-05-30 10:19:39.179315+08removed   | FalseSELECT 1Time: 0.001s
复制代码


其中 @> 表示的意思是:


查询 address 字段(一个 JSON 对象) 中 street 字段的值是 Article Circle Expressway 2


查询所有住在北极的信息(city = North Pole):


dudu@/tmp:t> SELECT * FROM contacts WHERE address @> '{"city": "North Pole"}';-[ RECORD 1 ]-------------------------id        | 1name      | Santa Clausaddress   | {"city": "North Pole", "state": "Alaska", "street": "Article Circle Expressway 1", "postcode": "99705"}create_at | 2022-05-30 10:19:39.179315+08update_at | 2022-05-30 10:19:39.179315+08removed   | False-[ RECORD 2 ]-------------------------id        | 2name      | dudu your neighboraddress   | {"city": "North Pole", "state": "Alaska", "street": "Article Circle Expressway 2", "postcode": "99705"}create_at | 2022-05-30 10:19:39.179315+08update_at | 2022-05-30 10:19:39.179315+08removed   | FalseSELECT 2Time: 0.001s
复制代码

Query using diesel

那么,通过 diesel 如何查询呢?


diesel 默认不支持 JSON 对象,因此我们需要自己实现一个 JSON 对象的查询。


我们可以使用 diesel_infix_operator 来实现一个 JSON 对象的查询,比如 @>:


use diesel::expression::AsExpression;use diesel::pg::Pg;
diesel_infix_operator!(Contains, " @> ", backend: Pg);
// Normally you would put this on a trait insteadfn contains<T, U>(left: T, right: U) -> Contains<T, U::Expression>where T: Expression, U: AsExpression<T::SqlType>,{ Contains::new(left, right.as_expression())}
pub fn get_contacts_by_address( conn: &PgConnection, address: &serde_json::Value,) -> QueryResult<Vec<Contact>> { use crate::schema::contacts::dsl::{address as contact_address, contacts};
contacts.filter(contains(contact_address, address)).get_results(conn)}
复制代码


其中:


  • diesel_infix_operator! 是一个宏,它的第一个参数是宏名,第二个参数是表达式,第三个参数是 backend,表示支持自定义表达式的数据库,此时使用 PostgreSQL

  • diesel_infix_operator! 将创建一个具有给定名称的新类型。它将实现在 Diesel 中用作表达式所需的所有方法,将给定的 SQL 放在两个元素之间。第三个参数指定运算符返回的 SQL 类型。如果没有给出,类型将被假定为 Bool。

  • 如果运算符 (operator) 特定于单个后端(如 PostgreSQL 或是 MySQL),您可以通过调用宏时添加 backend: Pg 来指定它。

  • 需要注意的是,生成的 impl 不会约束参数的 SQL 类型。您应该确保它们在构造运算符的函数中属于正确的类型。

  • diesel_infix_operator! 生成的类型不是 public 的类型。一般的做法是,创建一个 public 用于构造表达式的函数(或 trait),以及一个表示该函数的返回类型的辅助类型(参考 diesel 内置的 And Eq 等类型)。dieselExpressionMethods 这个 trait 定义了很多能够创建表达式的函数,如 eq, and, or 及他们对应的返回的类型如 Eq, And, Or,可以参考 diesel::expression::expression_methodsdiesel::expression::helper_types 的示例。https://docs.rs/diesel/latest/diesel/helper_types/index.html ,我们的例子中 Contains 类型就和 EqAnd 等类似。

  • 关于表达式到 sql 的转换,可以举个例子: false.and(false.or(true)) 会生成 FALSE AND (FALSE OR TRUE)


另外,我们可以通过 diesel::debug_query 来查看查询的 SQL:


pub fn get_contacts_by_address(    conn: &PgConnection, address: &serde_json::Value,) -> QueryResult<Vec<Contact>> {    use crate::schema::contacts::dsl::{address as contact_address, contacts};
// let sql = diesel::debug_query::<DB, _>(&users.count()).to_string(); let query = contacts.filter(contains(contact_address, address)); let debug = diesel::debug_query::<diesel::pg::Pg, _>(&query); println!("The insert query: {:#?}", debug); query.get_results(conn)}
复制代码


它会在调用 sql 之前输出 @> 运算符的 SQL


 Query {    sql: "SELECT \"contacts\".\"id\", \"contacts\".\"name\", \"contacts\".\"address\", \"contacts\".\"create_at\", \"contacts\".\"update_at\", \"contacts\".\"removed\" FROM \"contacts\" WHERE \"contacts\".\"address\" @> $1",    binds: [        Object({            "street": String(                "Article Circle Expressway 1",            ),        }),    ],}
复制代码


相当于下面的 sql 语句:


SELECT    "contacts"."id",    "contacts"."name",    "contacts"."address",    "contacts"."create_at",    "contacts"."update_at",    "contacts"."removed"FROM "contacts"WHERE ("contacts"."address" @> '{"street": "Article Circle Expressway 1"}')
复制代码


关于 diesel_infix_operator 可以参考:


https://docs.rs/diesel/latest/diesel/macro.diesel_infix_operator.html#example-usage

发布于: 刚刚阅读数: 3
用户头像

伍思默

关注

还未添加个人签名 2018.07.05 加入

还未添加个人简介

评论

发布
暂无评论
当 diesel 遇见 jsonb_伍思默_InfoQ写作社区