如何使用 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 contracts
pub 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 | 1
name | Santa Claus
address | {"city": "North Pole", "state": "Alaska", "street": "Article Circle Expressway 1", "postcode": "99705"}
create_at | 2022-05-30 08:28:04.744632+08
update_at | 2022-05-30 08:28:04.744632+08
removed | False
-[ RECORD 2 ]-------------------------
id | 2
name | Dudu Your Neighbor
address | {"city": "North Pole", "state": "Alaska", "street": "Article Circle Expressway 2", "postcode": "99705"}
create_at | 2022-05-30 08:28:04.744632+08
update_at | 2022-05-30 08:28:04.744632+08
re
SELECT 2
Time: 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 | 2
name | dudu your neighbor
address | {"city": "North Pole", "state": "Alaska", "street": "Article Circle Expressway 2", "postcode": "99705"}
create_at | 2022-05-30 10:19:39.179315+08
update_at | 2022-05-30 10:19:39.179315+08
removed | False
SELECT 1
Time: 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 | 1
name | Santa Claus
address | {"city": "North Pole", "state": "Alaska", "street": "Article Circle Expressway 1", "postcode": "99705"}
create_at | 2022-05-30 10:19:39.179315+08
update_at | 2022-05-30 10:19:39.179315+08
removed | False
-[ RECORD 2 ]-------------------------
id | 2
name | dudu your neighbor
address | {"city": "North Pole", "state": "Alaska", "street": "Article Circle Expressway 2", "postcode": "99705"}
create_at | 2022-05-30 10:19:39.179315+08
update_at | 2022-05-30 10:19:39.179315+08
removed | False
SELECT 2
Time: 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 instead
fn 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
评论