Toasty 是一个为 Rust 编程语言设计的异步 ORM,旨在简化使用过程。Toasty 支持 SQL 和 NoSQL 数据库,包括 DynamoDB 和 Cassandra(即将支持)。
Toasty 目前处于开发初期阶段,可以被视为“预览版”(尚未准备好用于实际生产环境)。它还未在 crates.io 上发布。我之所以现在宣布它,是因为我已经将 GitHub 仓库公开,并计划在公开环境中继续开发,希望能够获得反馈意见。
使用 Toasty 的项目首先需要创建一个 schema 文件,以定义应用程序的数据模型。例如,下面是 hello-toasty/schema.toasty 文件的内容:
rustCopy codemodel 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 代码。上面 schema 文件生成的代码可以在 这里 查看。
之后,您可以轻松地操作数据模型:
rustCopy code// 创建一个新用户,并为他们添加一些待办事项。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?;// 从数据库中加载用户let user = User::find_by_email("john@example.com").get(&db).await?// 加载并遍历用户的待办事项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 应用的库相对较少(尽管近年来这种情况有所改变)。此外,现有的许多库通常为了性能而牺牲了易用性。在 Rust 的生态系统中存在一个空白点:许多团队反映,当前 Rust 的 ORM 库在使用上的摩擦感很大(有的团队甚至选择实现自己的数据库抽象库来应对这种摩擦)。Toasty 旨在填补这一空白,专注于更高层次的应用场景,并优先考虑易用性,而不是性能最大化。
什么样的 ORM 才算易用?当然,这就是所谓的“百万美元问题”。Rust 社区仍在探索如何设计易用的库。Rust 的 traits 和生命周期特性非常强大,既能提高性能,也能实现有趣的模式(例如 typestate 模式)。然而,过度使用这些特性也会导致难以使用的库。
因此,在构建 Toasty 时,我尽量谨慎地使用这些特性,注重最小化 traits 和生命周期的使用。下面这段代码是从 Toasty 生成的代码中摘取的,我预计这是 95% 的 Toasty 用户可能会遇到的最复杂的类型签名。
rustCopy codepub 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 }}
其中确实包含了一个生命周期,以避免在查询构建器中复制数据。根据用户反馈,我可能会完全去掉生命周期。
易用性的另一个方面是减少样板代码。Rust 已经在这方面有一个非常出色的功能:过程宏。大多数人可能已经使用过 Serde,因此应该了解它的优势。然而,对于 Toasty,我选择暂时不使用过程宏,至少在初期不使用。
过程宏会在构建时生成大量隐藏代码。对于像 Serde 这样的库,这并不是问题,因为 Serde 宏生成的是公共 trait(Serialize 和 Deserialize)的实现。Serde 用户实际上不需要了解这些 trait 的实现细节。
而 Toasty 的情况有所不同。Toasty 会生成大量您将直接使用的公共方法和类型。在 “Hello Toasty” 示例中,Toasty 生成了 User::find_by_email 方法。与过程宏不同,我选择了显式代码生成步骤,Toasty 会将代码生成到文件中,您可以打开并阅读。Toasty 会尽量使生成的代码保持可读性,以便更容易发现生成的方法。这种可发现性将使库更加易用。
Toasty 目前仍处于开发早期阶段,API 将根据您的反馈不断演变。最终,如果您在使用过程中遇到摩擦,请告诉我,我会努力改进。
支持 SQL 和 NoSQLToasty 支持 SQL 和 NoSQL 数据库。截至目前,这意味着支持 Sqlite 和 DynamoDB,尽管增加对其他 SQL 数据库的支持应该相对容易。我也计划很快添加对 Cassandra 的支持,但也希望其他人能为不同数据库的实现做出贡献。
需要明确的是,Toasty 虽然同时支持 SQL 和 NoSQL 数据库,但并不会抽象掉目标数据库。一个使用 SQL 数据库编写的应用程序无法直接运行在 NoSQL 数据库上。相反,Toasty 也不会抽象掉 NoSQL 数据库,您需要理解如何建模 schema 才能充分利用目标数据库。我的观察是,大多数数据库库在处理不同的后台数据存储时,其核心功能基本一致:数据到结构体的映射以及执行基本的 Get、Insert 和 Update 查询。
Toasty 从这一标准功能集入手,并在可选的基础上暴露数据库特定的功能。它还将通过对生成的查询方法进行选择,帮助您避免在目标数据库上执行低效查询。
下一步您应该尝试使用 Toasty,运行示例并进行实验。目前,Toasty 仍在积极开发中,尚未准备好投入实际使用。下一步的目标是填补现有的功能空白。我计划在明年年底之前(更现实的时间点)让 Toasty 准备好投入生产使用。
此外,Toasty 试图以当前的方式同时支持 SQL 和 NoSQL 是一种新的尝试(据我所知)。如果您了解类似的尝试,特别是过去的尝试中遇到的困难,我非常乐意听取您的意见。我也知道,许多人对数据库、ORM 等方面有强烈的观点,我期待这些讨论。可以在 Tokio Discord 的 #toasty 频道讨论。另外,也可以在 Github 仓库 上创建问题,提出功能建议或讨论 API 设计和方向。
——Carl Lerche