宣布 Toasty,Rust 的异步 ORM
2024 年 10 月 23 日
Toasty 是 Rust 编程语言的异步 ORM,优先考虑易用性。Toasty 支持 SQL 和 NoSQL 数据库,包括 DynamoDB 和 Cassandra(即将推出)。
Toasty 目前处于开发的早期阶段,应被视为“预览版”(尚未准备好用于实际应用)。它也尚未在 crates.io 上发布。我现在宣布它是因为我已经公开了 Github 存储库,将继续公开开发,并希望获得反馈。
使用 Toasty 的项目首先创建一个模式文件来定义应用程序的数据模型。例如,这是 hello-toasty/schema.toasty
文件的内容。
model User {
#[key]
#[auto]
id: Id,
name: String,
#[unique]
email: String,
todos: [Todo],
moto: Option<String>,
}
model Todo {
#[key]
#[auto]
id: Id,
#[index]
user_id: Id<User>,
#[relation(key = user_id, references = id)]
user: User,
title: String,
}
使用 Toasty CLI 工具,您将生成所有必要的 Rust 代码以处理此数据模型。上面模式生成的代码在此处。
然后,您可以轻松地处理数据模型
// Create a new user and give them some todos.
User::create()
.name("John Doe")
.email("john@example.com")
.todo(Todo::create().title("Make pizza"))
.todo(Todo::create().title("Finish Toasty"))
.todo(Todo::create().title("Sleep"))
.exec(&db)
.await?;
// Load the user from the database
let user = User::find_by_email("john@example.com").get(&db).await?
// Load and iterate the user's todos
let mut todos = user.todos().all(&db).await.unwrap();
while let Some(todo) = todos.next().await {
let todo = todo.unwrap();
println!("{:#?}", todo);
}
为什么选择 ORM?
从历史上看,Rust 一直被定位为系统级编程语言。在服务器端,Rust 在数据库、代理和其他基础设施级应用等用例中增长最快。然而,在与为这些基础设施级用例采用 Rust 的团队交谈时,经常听到他们开始更频繁地将 Rust 用于更高级别的用例,例如更传统的 Web 应用程序。
通常的观点是在性能不太关键时最大化生产力。我同意这种观点。在构建 Web 应用程序时,性能是次要考虑因素,生产力才是主要的。那么,为什么团队在性能不太关键的情况下更频繁地采用 Rust 呢?那是因为一旦你学会了 Rust,你就可以非常高效。
生产力是复杂且多方面的。我们都同意 Rust 的编辑-编译-测试周期可以更快。这种摩擦被更少的错误、生产问题和强大的长期维护故事所抵消(Rust 的借用检查器倾向于激励更可维护的代码)。此外,由于 Rust 可以很好地用于许多用例,无论是基础设施级服务器用例、更高级别的 Web 应用程序,还是客户端(通过 WASM 的浏览器以及 iOS、MacOS、Windows 等原生平台),Rust 都具有出色的代码重用性。内部库可以编写一次并在所有这些上下文中重用。
因此,虽然 Rust 可能不是原型设计最高效的编程语言,但对于将存在多年的项目来说,它非常有竞争力。
好的,那么为什么选择 ORM 呢?针对给定用例的全功能库生态系统是生产力难题的重要组成部分。Rust 拥有充满活力的生态系统,但历史上更多地关注基础设施级用例。较少的库针对更高级别的 Web 应用程序用例(尽管最近情况正在发生变化)。此外,今天存在的许多库都强调最大化性能而牺牲易用性的 API。Rust 的生态系统中存在差距。许多我交谈过的团队报告说,Rust ORM 库的当前状态是一个很大的摩擦点(不止一个团队选择实施其内部数据库抽象来处理这种摩擦)。Toasty 旨在通过关注更高级别的用例并将易用性置于最大化性能之上来填补这一空白。
是什么使 ORM 易于使用?
当然,这是一个价值百万美元的问题。Rust 社区仍在研究如何设计易于使用的库。Rust 的 traits 和 lifetimes 非常引人注目,可以提高性能,并启用有趣的模式(例如,typestate 模式)。但是,过度使用这些功能也会导致库难以使用。
因此,在构建 Toasty 时,我试图对此保持敏感,并专注于最少程度地使用 traits 和 lifetimes。此代码片段来自 Toasty 从模式文件生成的代码,我预计这将是 95% 的 Toasty 用户遇到的最复杂的类型签名。
pub fn find_by_email<'a>(
email: impl stmt::IntoExpr<'a, String>
) -> FindByEmail<'a> {
let expr = User::EMAIL.eq(email);
let query = Query::from_expr(expr);
FindByEmail { query }
}
这确实包含一个 lifetime 以避免将数据复制到查询构建器中,但我对此仍持观望态度。根据用户反馈,我将来可能会完全删除 lifetimes。
易用性的另一个方面是最小化样板代码。Rust 已经为此提供了一个杀手级功能:过程宏。你们大多数人已经使用过 Serde,所以你们知道这有多么令人愉快。话虽如此,我选择不为 Toasty 使用过程宏,至少在最初阶段不使用。
过程宏在构建时生成大量隐藏代码。对于像 Serde 这样的库来说,这没什么大不了的,因为 Serde 宏生成公共 traits(Serialize 和 Deserialize)的实现。Serde 的用户实际上并不需要了解这些 traits 的实现细节。
Toasty 的情况有所不同。Toasty 将生成许多您将直接使用的公共方法和类型。在“Hello Toasty”示例中,Toasty 生成了 User::find_by_email
方法。我没有使用过程宏,而是使用了显式的代码生成步骤,Toasty 将代码生成到您可以打开和读取的文件中。Toasty 将尝试使生成的代码尽可能可读,以便轻松发现生成的方法。这种增加的可发现性将使库更易于使用。
Toasty 仍处于开发的早期阶段,API 将根据您的反馈不断发展。归根结底,如果您遇到摩擦,我想听到它并修复它。
SQL 和 NoSQL
Toasty 同时支持 SQL 和 NoSQL 数据库。截至今天,这意味着 Sqlite 和 DynamoDB,尽管添加对其他 SQL 数据库的支持应该非常简单。我还计划很快添加对 Cassandra 的支持,但我希望其他人也能为不同数据库的实现做出贡献。
需要明确的是,Toasty 同时适用于 SQL 和 NoSQL 数据库,但不会抽象掉目标数据库。使用 Toasty 为 SQL 数据库编写的应用程序不会透明地在 NoSQL 数据库上运行。反之亦然,Toasty 不会抽象掉 NoSQL 数据库,您需要了解如何建模您的模式以利用目标数据库。我在数据库库中注意到的是,每个库的大部分工作都是相同的,无论后端数据存储是什么:将数据映射到结构体并发出基本的 Get、Insert 和 Update 查询。
Toasty 从这个标准功能集开始,并以选择加入的方式公开特定于数据库的功能。它还将通过有选择地生成查询方法,帮助您避免为目标数据库发出低效的查询。
下一步
您应该尝试 Toasty,尝试示例并进行实验。今天,Toasty 仍处于积极开发中,尚未准备好用于实际应用。当前的下一步将是填补这些空白。我的目标是在明年某个时候(实际上是在年底)让 Toasty 准备好用于实际应用。
此外,尝试以 Toasty 的方式支持 SQL 和 NoSQL 是新颖的(据我所知)。如果您知道先前的技术,特别是以前的尝试遇到的陷阱,我很乐意听到。我也知道你们许多人对使用数据库、ORM 等有强烈的看法,我期待着这些讨论。Tokio Discord 中有一个 #toasty 频道用于讨论。此外,欢迎在 Github 仓库上创建 issue 以提出功能建议或开始关于 API 设计和方向的对话。