RUST异步数据库工具库sqlx快速入门教程
数据最终还是要落到某个地方的,很多时候都是关系型数据库,所以数据库的学习是必不可少的, 本教程主要记录sqlx的一些常用代码片段,便于快速掌握sqlx的增删改查操作(CRUD)。
值得注意的是,sqlx不是ORM, 所以必须自己写sql, 如果不喜欢自己写sqlx可以选择rust的其他orm, 比如
ormx
或者diesel
版本说明
Cargo.toml文件里的依赖配置如下:
[dependencies]
tokio = {version = "1", features=["full"] }
sqlx = { version = "0.7", features = [ "runtime-tokio", "postgres"] }
注意: 这里使用的sqlite, 如果需要使用其他数据库, 比如mysql, postgresql需要开启必要的特性。
安装sqlx-cli
sqlx-cli是一个sqlx配套的命令行工具,可以管理数据库。
cargo install sqlx-cli
本文使用的版本是: 0.7.2
通过查看是否可以执行sqlx
命令检查是否安装成功。
数据库管理
我们只可以直接通过sqlx-cli创建和删除数据库, 在操作数据库之前你需要设置环境变量DATABASE_URL
, 比如DATABASE_URL=postgres://用户名:密码@数据库地址/数据库名
如果是linux环境,可以通过以下命令设置环境变量(windows的git bash也行)
export DATABASE_URL=postgres://用户名:密码@数据库地址/数据库名
将配置放在.env
文件似乎也可以, 但是有时候不生效, 不知道为啥。
数据库管理
当前DATABASE_URL
变量设置完毕, 就可以通过sqlx命令行工具管理数据库了。
创建
我们可以通过以下命令创建
sqlx database create
删除
sqlx database drop
处理创建和删除还有其他命令reset
和setup
, 这个大家可以通过sqlx help database
查看具体的使用说明
数据表管理
当数据库创建完成,就可以管理数据表了,假设我们想要管理一张todo的表,创建的sql如下。
CREATE TABLE IF NOT EXISTS todos
(
id BIGSERIAL PRIMARY KEY,
description TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT FALSE
);
migration
由于开发过程中,数据表的改动时常发生,所以我们需要一套版本控制的流程,大多数的ORM都将这个操作叫做migration
(不知道翻译成啥比较好),sqlx也不例外, 有了这套工具我们就应该每次创建一个新的版本,然后有sqlx来管理数据表之间的不同,以及记录创建表的sql代码的各个版本。
执行以下命令创建第一个版本
sqlx migrate add todo
它会在当前目录创建一个migrations
的目录, 然后创建一个{时间}_todo.sql
的文件, 时间用于确定各个sql的先后顺序,然后我们可以将上面的sql语句复制到{时间}_todo.sql
文件。
创建数据表
这条命令会创建最新的sql语句,数据表的修改需要自己维护sql
sqlx migrate run
注意: 每次修改sql都需要执行sqlx migrate add todo来创建一个新的sql
代码管理数据库和数据表
sqlx的数据库和数据表除了使用sqlx-cli命令行操作,也可以使用代码管理。
use sqlx::{migrate::MigrateDatabase, Postgres, postgres::PgPoolOptions};
const DB_URL: &str = "postgres://用户名:密码@数据库地址/数据库名";
#[tokio::main]
async fn main() {
// 判断数据库是否存在,不存在则创建
if !Postgres::database_exists(DB_URL).await.unwrap_or(false) {
println!("创建数据库 {}", DB_URL);
match Postgres::create_database(DB_URL).await {
Ok(_) => println!("创建数据库成功"),
Err(err) => panic!("创建数据库失败: {}", err)
}
} else {
println!("创建库已存在, 无需创建");
}
// 创建连接池
let db = PgPoolOptions::new()
// 设置最大连接数
.max_connections(20)
.connect(DB_URL)
.await.unwrap();
// 执行创建表的sql
let result = sqlx::query(r#"
CREATE TABLE IF NOT EXISTS todos
(
id BIGSERIAL PRIMARY KEY,
description TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT FALSE
);"#)
.execute(&db)
.await
.unwrap();
println!("创建数据库的结果: {:?}", result);
}
数据管理
当数据库和数据表都有了,自然就可以增删改查数据了。不过,在此之前应该了解如何连接数据库。
连接数据库
use sqlx::postgres::PgPoolOptions;
const DB_URL: &str = "postgres://用户名:密码@数据库地址/数据库名";
#[tokio::main]
async fn main() {
// 创建连接池
let db = PgPoolOptions::new()
// 设置最大连接数
.max_connections(20)
.connect(DB_URL)
.await.unwrap();
}
增删改查
有了连接就可以对数据增删改查了。
use sqlx::postgres::PgPoolOptions;
const DB_URL: &str = "postgres://用户名:密码@数据库地址/数据库名";
#[tokio::main]
async fn main() {
// 创建连接池
let db = PgPoolOptions::new()
// 设置最大连接数
.max_connections(20)
.connect(DB_URL)
.await.unwrap();
// 插入数据
let result = sqlx::query!(
r#"
INSERT into todos (description)
values($1)
RETURNING id
"#,
"hello world")
.fetch_one(&db)
.await
.expect("插入数据失败.");
println!("插入数据成功, 对应的id是: {:?}", result.id);
// 查询数据
let result = sqlx::query!(r#"SELECT * from todos"#,)
.fetch_all(&db)
.await
.expect("查询数据失败.");
for row in result {
println!("查询数据结果: [{}] {} {}", row.id, row.description, row.done);
}
// 更新数据
let result = sqlx::query!(
r#"
UPDATE todos SET description = $1
WHERE ID = 1
"#,
"updated")
.execute(&db)
.await
.expect("更新数据失败.");
println!("更新的结果: {:?}", result);
// 删除数据
let result = sqlx::query!(
r#"
DELETE FROM todos WHERE ID = $1
"#,
1)
.execute(&db)
.await
.expect("删除数据失败.");
println!("删除的结果: {:?}", result);
}
输出的结果如下:
插入数据成功, 对应的id是: 1
查询数据结果: [1] hello world false
更新的结果: PgQueryResult { rows_affected: 1 }
删除的结果: PgQueryResult { rows_affected: 1 }
绑定结构体
虽然上面的代码可以解决问题,但是,很多时候我们会将结果绑定到结构体中。
use sqlx::postgres::PgPoolOptions;
use sqlx::FromRow;
const DB_URL: &str = "postgres://用户名:密码@数据库地址/数据库名";
#[derive(Debug, FromRow)]
struct Todo {
id: i64,
description: String,
done: bool
}
#[tokio::main]
async fn main() {
// 创建连接池
let db = PgPoolOptions::new()
// 设置最大连接数
.max_connections(20)
.connect(DB_URL)
.await.unwrap();
// 插入数据
let result = sqlx::query!(
r#"
INSERT into todos (description)
values($1)
RETURNING id
"#,
"hello world")
.fetch_one(&db)
.await
.expect("插入数据失败.");
println!("插入数据成功, 对应的id是: {:?}", result.id);
// 查询数据
let result = sqlx::query_as!(Todo, r#"SELECT * from todos"#)
.fetch_all(&db)
.await
.expect("查询数据失败.");
for todo in result {
println!("查询数据结果: [{}] {} {}", todo.id, todo.description, todo.done);
}
}
这段代码的重点在于, 首先有一个存在数据的结构体,它需要至少使用sqlx提供的FromRow
, 比如#[derive(FromRow)]
,最后就是需要在查询的时候在query_as!
的第一个参数指定对应的类型。
插入多行数据
如果一次查询多条数据会稍微麻烦一点
use sqlx::postgres::PgPoolOptions;
use sqlx::FromRow;
const DB_URL: &str = "postgres://用户名:密码@数据库地址/数据库名";
#[derive(Debug, FromRow)]
struct Todo {
id: i64,
description: String,
done: bool
}
#[tokio::main]
async fn main() {
// 创建连接池
let db = PgPoolOptions::new()
// 设置最大连接数
.max_connections(20)
.connect(DB_URL)
.await.unwrap();
// 摘自: https://github.com/launchbadge/sqlx/issues/294#issuecomment-830409187
let lala = vec![("abc", true), ("xyz", false)];
let mut v1: Vec<String> = Vec::new();
let mut v2: Vec<bool> = Vec::new();
lala.into_iter().for_each(|todo| {
v1.push(todo.0.into());
v2.push(todo.1);
});
let result = sqlx::query(
r#"INSERT INTO todos (description, done)
SELECT * FROM UNNEST($1, $2)"#,
)
.bind(&v1)
.bind(&v2)
.execute(&db)
.await
.expect("插入多行数据失败");
println!("插入多行数据结果: {:?}", result);
}
总结
直接使用sql并没有想象中的那么复杂,这是因为本篇的sql都比较简单,对于比较复杂的事务肯定是写起来比较烦人的,这个可以考虑使用ORM或者将sql写到一个文件,然后使用sqlx提供的query_file_as
之类的宏来处理。