如何使用 diesel 来操作 jsonb 类型的数据呢?
diesel cli 和 diesel migration
首先,我们需要安装 diesel:
其次,我们运行 diesel migration generate 命令来创建数据库表。
我们用一个名为 contacts 的表来存储联系人信息,它包含一个 jsonb 类型的字段,我们将在这个字段中存储联系人的信息:
diesel migration generate contacts
复制代码
以上命令会在 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 文件中删除表:
这样在下次运行 diesel migration revert 命令时,数据库表将被删除。
编辑好 up.sql 之后,我们可以运行 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 等类型)。diesel 里 ExpressionMethods 这个 trait 定义了很多能够创建表达式的函数,如 eq, and, or 及他们对应的返回的类型如 Eq, And, Or,可以参考 diesel::expression::expression_methods 和 diesel::expression::helper_types 的示例。https://docs.rs/diesel/latest/diesel/helper_types/index.html ,我们的例子中 Contains 类型就和 Eq,And 等类似。
关于表达式到 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
评论