一个新的方式使用Rust操作数据库
1 简介
Rust 和数据库交互的一个新的操作库,现有的库没有提供足够的编译时保证,并且像 SQL 一样冗长或笨拙。
我如此关心的原因是数据库真的很酷。它们解决了制作支持原子事务的抗崩溃软件的巨大问题。
结构化查询语言 (SQL) 是一种协议对于那些不知道的人来说,SQL 是与数据库交互的标准。以至于几乎所有数据库都只接受某种 SQL 方言的查询。
我的观点是 SQL 应该供计算机编写。这将使它与 LLVM IR 牢牢地归为同一类别。它是人类可读的,这一事实对于调试和测试很有用,但我不认为这是你想要的查询编写方式。
- rust-query
rust-query 是对 Rust 中关系数据库查询的回答。它是一个固执己见的库,与 Rust 的类型系统深度集成,使数据库操作感觉像 Rust 原生。
主要特点和设计决策
显式表别名:联接表会返回一个表示该表的虚拟对象。let user = User::join(rows);
Null 安全性:查询中的可选值具有 type,需要特别小心处理。Option
直观的聚合:我们的聚合保证为它们联接的每一行提供单个结果。尝试后,您会发现这比传统操作直观得多。GROUP BY
类型安全的外键导航:数据库约束类似于类型签名,因此您可以通过外键(例如)易于使用的隐式连接来依赖它们进行查询。track.album().artist().name()
类型安全的唯一查找:例如可以使用 .Option<Rating>Rating::unique(my_user, my_story)
多版本架构:它是声明性的,您可以立即看到架构的所有过去版本之间的差异!
类型安全迁移:迁移具有查询的所有功能,并且可以使用任意 Rust 代码来处理行。是否曾经不得不查阅数据库之外的东西才能在迁移中使用?现在你可以了!
类型安全的唯一冲突:在具有唯一约束的表中插入和更新行会导致专门的错误类型。
与事务生命周期相关的行引用:只有在保证行存在时,才能使用行引用。
封装的类型化行 ID:实际的行号永远不会从库 API 中公开。应用程序 logic 不需要了解它们。
2 执行和操作
您始终从定义架构开始。这样,以后就可以轻松迁移到不同的架构。
#[schema]
enum Schema {
User {
name: String,
},
Story {
author: User,
title: String,
content: String
},
#[unique(user, story)]
Rating {
user: User,
story: Story,
stars: i64
},
}
use v0::*;
中的架构定义使用 enum 语法,但此处未定义实际的 enum。 此架构定义了三个具有指定列和关系的表:rust-query
使用另一个表名作为列类型会创建外键约束。
该属性创建命名的唯一约束。
该宏解析枚举语法并生成包含数据库 API 的模块。
3 编写查询
首先,让我们看看如何将一些数据插入到我们的 schema 中:
fn insert_data(txn: &mut TransactionMut<Schema>) {
// Insert users
let alice = txn.insert(User {
name: "alice",
});
let bob = txn.insert(User {
name: "bob",
});
// Insert a story
let dream = txn.insert(Story {
author: alice,
title: "My crazy dream",
content: "A dinosaur and a bird...",
});
// Insert a rating - note the try_insert due to the unique constraint
let rating = txn.try_insert(Rating {
user: bob,
story: dream,
stars: 5,
}).expect("no rating for this user and story exists yet");
}
关于插入的几个要点:
我们需要一个可变的事务 (TransactionMut) 来修改数据库。
Insert 操作返回对新插入的行的引用。
当插入到具有唯一约束的表中时,try_insert用于处理潜在的冲突。
try_insert的错误类型取决于表具有的唯一约束数。
现在让我们查询此数据:
fn query_data(txn: &Transaction<Schema>) {
let results = txn.query(|rows| {
let story = Story::join(rows);
let avg_rating = aggregate(|rows| {
let rating = Rating::join(rows);
rows.filter_on(rating.story(), &story);
rows.avg(rating.stars().as_float())
});
rows.into_vec((story.title(), avg_rating))
});
for (title, avg_rating) in results {
println!("story '{title}' has avg rating {avg_rating:?}");
}
}
有关查询的要点:
rows表示查询中的当前行集。
联接可以添加行,筛选器可以删除行。
- 详细信息
aggregate用于计算聚合不会更改查询中的行数。
rows.filter_on可用于筛选聚合中的行以匹配外部范围中的值。
rows.avgNone该方法返回聚合中行的平均值,如果没有行,则平均值的计算结果为。
结果可以收集到元组或结构的向量中。
架构演变和迁移。
假设您要为每个用户添加一个电子邮件地址。以下是创建新架构版本的方法:
#[schema]
#[version(0..=1)]
enum Schema {
User {
name: String,
#[version(1..)]
email: String,
},
// ... rest of schema ...
}
use v1::*;
以下是迁移数据的方法:
let m = m.migrate(v1::update::Schema {
user: Box::new(|old_user| {
Alter::new(v1::update::UserMigration {
email: old_user
.name()
.map_dummy(|name| format!("{name}@example.com")),
})
}),
});
该模块包含定义v1 schema 和 v0 schema 之间差异的结构.
我们使用这些结构体来实现迁移。这样,将针对旧架构和新架构对迁移进行类型检查。
请注意,在迁移中,我们可以执行我们想要的所有单行查询:聚合、唯一约束查找等!
我们还可以与任意 Rust 一起使用来进一步处理map_dummy行。
4 结论
rust-query代表了 Rust 中数据库交互的新方法,优先考虑:
在编译时检查所有可能的内容。
使彼此和任意 Rust 组合查询成为可能。
通过类型检查迁移启用架构演变。
- 点赞
- 收藏
- 关注作者
评论(0)