Rust中异步ORM:Toasty

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 的特征和生命周期引人注目,可以提高性能,并实现有趣的模式(例如,typestate 模式)。然而,过度使用这些功能也会导致库难以使用。

因此,在构建 Toasty 时,我尝试对此保持敏感,并专注于尽量减少使用特征和生命周期。此代码片段来自 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 }
}

这确实包括一个生命周期,以避免将数据复制到查询生成器中,但我对此仍持观望态度。根据用户反馈,我可能会在未来完全删除生命周期。

易用性的另一个方面是尽量减少样板代码。Rust 已经为此提供了一项杀手级功能:过程宏。你们大多数人已经使用过 Serde,所以你们知道这有多么令人愉快。话虽如此,我选择不对 Toasty 使用过程宏,至少最初不会。

过程宏在构建时会生成大量隐藏代码。这对于 Serde 之类的库来说不是什么大问题,因为 Serde 宏会生成公共特征(Serialize 和 Deserialize)的实现。Serde 的用户实际上并不需要知道这些特征的实现细节。

Toasty 则不同。Toasty 将生成许多您将直接使用的公共方法和类型。在“Hello Toasty”示例中,Toasty 生成方法 User::find_by_email。我没有使用过程宏,而是使用了显式代码生成步骤,Toasty 将代码生成到您可以打开和阅读的文件中。Toasty 将尝试使生成的代码尽可能易于阅读,以便轻松发现生成的方法。这种增加的可发现性将使库更易于使用。

Toasty 仍处于开发初期,API 将根据您的反馈进行改进。最终,如果您遇到问题,我希望了解并解决它。

SQL 和 NoSQL
Toasty 支持 SQL 和 NoSQL 数据库。截至目前,这意味着 Sqlite 和 DyanmoDB,尽管添加对其他 SQL 数据库的支持应该相当简单。我还计划很快添加对 Cassandra 的支持,但我希望其他人也能为不同数据库的实现做出贡献。

需要明确的是,Toasty 适用于 SQL 和 NoSQL 数据库,但不会抽象 目标数据库。使用 Toasty 为 SQL 数据库编写的应用程序不会透明地在 NoSQL 数据库上运行。相反,Toasty 不会抽象 NoSQL 数据库,您需要了解如何对架构进行建模以利用目标数据库。我注意到,对于数据库库,无论后端数据存储如何,每个库的大多数功能都相同:将数据映射到结构并发出基本的 Get、Insert 和 Update 查询。

Toasty 从这套标准功能开始,并根据用户选择公开特定于数据库的功能。它还可通过选择生成的查询方法,帮助您避免对目标数据库发出低效查询。