“有时候,优雅的实现只是一个函数。不是一种方法、一个类或者一个框架,只是一个函数。” —— John Carmack

postgres

在 Cargo.toml 中添加 postgres 依赖(目前最新版本是 0.19.2):

1
2
[dependencies]
postgres = "0.19.2"

main.rs 中引入依赖:

=> postgres 库文档 0.19.2 传送门

1
use postgres::{Client, NoTls};

由于 connect 带有错误处理,返回的 Rusult,因此 main 需要返回 Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
use postgres::{Client, NoTls};

fn main() -> Result<(), postgres::Error> {
let mut client = Client::connect("host=localhost user=postgres password=123456", NoTls)?;

client.batch_execute("
CREATE TABLE person (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
data BYTEA
)
")?;

let name = "Ferris";
let data = None::<&[u8]>;
client.execute(
"INSERT INTO person (name, data) VALUES ($1, $2)",
&[&name, &data],
)?;

for row in client.query("SELECT id, name, data FROM person", &[])? {
let id: i32 = row.get(0);
let name: &str = row.get(1);
let data: Option<&[u8]> = row.get(2);

println!("found person: {} {} {:?}", id, name, data);
}

Ok(())
}

? 是 Rust 的语法糖,使用 Rusult 的时候,我们一般都会在成功的时候提取值,而出现错误的时候提前返回 return 给调用方。按照常规的方法,就需要使用 match 进行模式匹配:

1
2
3
4
5
6
7
8
9
10
11
use std::string::FromUtf8Error;

fn str_upper_match(str: Vec<u8>) -> Result<String, FromUtf8Error> {
let ret = match String::from_utf8(str) {
Ok(str) => str.to_uppercase(),
Err(err) => return Err(err) // 提前返回
};

println!("Conversion succeeded: {}", ret);
Ok(ret)
}

? 则可以简化这种套路模板代码,因此上面代码可以简化为:

1
2
3
4
5
fn str_upper_match(str: Vec<u8>) -> Result<String, FromUtf8Error> {
let ret = String::from_utf8(str).map(|s| s.to_uppercase())?;
println!("Conversion succeeded: {}", ret);
Ok(ret)
}

r2d2 连接池

如果每次发生数据库事务,都要进行打开和关闭数据库连接,那么很快就会成为“瓶颈”。建立数据库连接是一项开销昂贵的操作,需要进行 TCP 握手操作。Rust 提供了 r2d2 软件包,提供了维护数据库连接池的通用方法。

引入依赖:

1
2
3
postgres = "0.19.2"
r2d2 = "0.8.9"
r2d2_postgres = "0.18.1"

r2d2 的体系结构由两个部分组成:通用部分和兼容各后端部分。后端代码通过实现 ManageConnection trait 实现特定数据库的连接和健康状态的检查。

1
2
3
4
5
6
7
8
9
10
11
pub trait ManageConnection: Send + Sync + 'static {
type Connection: Send + 'static;

type Error: error::Error + 'static;

fn connect(&self) -> Result<Self::Connection, Self::Error>;

fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error>;

fn has_broken(&self, conn: &mut Self::Connection) -> bool;
}

根据这个 trait 定义,我们需要指定一个 Connection 类型,它必须是 Send'static,以及 Error 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
use std::thread;
use r2d2;
use r2d2_postgres::{PostgresConnectionManager, postgres::NoTls};
use std::time::Duration;

const DROP_TABLE: &str = "DROP TABLE IF EXISTS books";
const CREATE_TABLE: &str = "CREATE TABLE IF NOT EXISTS books(
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
author VARCHAR NOT NULL,
year SERIAL
)";

#[derive(Debug)]
struct Book {
id: i32,
title: String,
author: String,
year: i32,
}

fn main() {
let manager = PostgresConnectionManager::new("host=localhost user=postgres password=123456".parse().unwrap(), NoTls);
let pool = r2d2::Pool::new(manager).unwrap();
let mut conn = pool.get().unwrap();

let _ = conn.execute(DROP_TABLE, &[]).unwrap();
let _ = conn.execute(CREATE_TABLE, &[]).unwrap();

thread::spawn(move || {
let book = Book {
id: 3,
title: "Mathematics".to_string(),
author: "Dr. John Smith".to_string(),
year: 2021,
};
conn.execute("INSERT INTO books (title, author, year) VALUES ($1, $2, $3)", &[&book.title, &book.author, &book.year]).unwrap();
});

thread::sleep(Duration::from_millis(100));

for _ in 0..10 {
let pool = pool.clone();
let mut client = pool.get().unwrap();
thread::spawn(move || {
for row in &client.query("SELECT id, title, author, year FROM books", &[]).unwrap() {
let book = Book {
id: row.get(0),
title: row.get(1),
author: row.get(2),
year: row.get(3)
};
println!("{:?}", book);
}
});
}
}

diesel ORM

使用具有原始 SQL 查询的低级数据库软件包编写复杂应用程序是一种容易出错的解决方案。diesel 是 Rust 的 ORM(对象关系映射器)和查询构建器。它采用了大量过程宏,会在编译期检测大多数数据库交互错误,并在大多数情况下能够生成非常高效的代码,有时甚至可以用C语言进行底层访问。这是因为它能够将通常在运行时进行的检查移动到编译期。

=> diesel ORM 官网

diesel 项目由许多组件构成。首先,我们有一个 diesel-cli 的工具,它将会设置数据库和其中的表。可以使用下面命令进行安装:

1
cargo install diesel_cli

如果只需要使用 PostgreSQL,可以直接指定 postgres 依赖,而不安装其它的诸如 MySQL 的依赖:

1
cargo install diesel_cli --no-default-features --features postgres

解决 1181 错误

这里有点坑,编译 diesel_cli 出现了错误

1
Compiling diesel_cli v1.4.1 error: linking with `link.exe` failed: exit code: 1181
  • 编译需要用到 libpq.lib。原本我的 PostgreSQL 是安装在 Docker 里的,重新在系统上安装了一遍。需要将 libpq.lib (PostgreSQL 安装目录下的 lib 中)拷贝到 C:\Users\xxxx.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib(建议使用 Everything 直接搜索) 目录下。
  • 需要将 PostgreSQL 安装目录中 bin 文件夹下的 *.dll 文件拷贝到 项目目录 中,否则 desel_cli 运行的时候会有运行时的动态链接错误。

设置数据库环境

接下来,[Linux 下] 需要将数据库的连接信息添加到项目根目录的 .env 文件下,Windows 下使用 set

1
echo DATABASE_URL=postgres://postgres:123456@localhost/diesel_demo > .env

运行 diesel setup 命令。如果 diesel_demo 数据库不存在,会自动创建。也可以直接使用 --database-url 指定连接:

1
diesel setup --database-url=postgres://postgres:123456@localhost/diesel_demo

创建和迁移数据库

使用 diesel migration generate create_users_table 命令可以添加一个创建数据库表的迁移命令

可以看到项目会出现一个 migrations 文件夹,并且有对应的 create_users_table 文件夹,里面生成了 up.sql 和 down.sql。在 up.sql 中可以写入创建该数据库表的 SQL 语句,down.sql 则相应的完成删除操作。

例如在 up.sql 中写入:

1
2
3
4
5
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
favorite_color VARCHAR
);

则在 down.sql 写入:

1
DROP TABLE USERS;

现在只需要运行 diesel migration run 就会自动创建数据库表,并且从数据库中读取信息生成 schema.rs 文件。

1
2
3
4
5
6
7
table! {
users (id) {
id -> Int4,
name -> Varchar,
favorite_color -> Nullable<Varchar>,
}
}
  • diesel migration generate:创建迁移文件夹和 up.sql 和 down.sql
  • diesel migration run:迁移
  • diesel migration revert:运行 down.sql
  • diesel migration redo:运行 down.sql,然后运行 up.sql

推荐参考官方入门教程

模型层

为了将数据库数据转换成 Rust 的数据结构,我们会编写模型层,按照惯例,会放到 model.rs 中:

1
2
3
4
5
6
7
#[derive(Queryable)]
pub struct User {
pub id: i32,
pub name: String,
pub favorite_color: String,
pub active: bool,
}

使用 r2d2 连接池

1
2
3
4
5
6
7
8
9
10
use diesel::r2d2::{ConnectionManager, Pool, PoolError};
type PgPool = Pool<ConnectionManager<PgConnection>>;

pub fn create_pool() -> Result<PgPool, PoolError> {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let manager = ConnectionManager::<PgConnection>::new(database_url);

PgPool::builder().build(manager)
}

额,diesel 的门槛确实有一点高。

小结

用下来,感觉 Rust 的生态很一般… diesel 说实话有点难用。