与 Diesel 对比
这是一个不可避免且富有争议的话题,SeaORM 与 Diesel 之间的技术差异已被广泛讨论。因此,我想指出我们刻意做出的一些不同设计选择,旨在让使用 SeaORM 的体验更愉快。
异步优先
SeaORM 从一开始就提供一流的异步支持。你不需要引入连接池依赖(如 r2d2)。我们为流行的 Web 框架提供了许多示例,它们是我们 CI 的一部分,因此永远不会出问题。
- Actix Example
- Axum Example
- GraphQL Example
- jsonrpsee Example
- Loco TODO Example / Loco REST Starter
- Poem Example
- Rocket Example / Rocket OpenAPI Example
- Salvo Example
- Tonic Example
- Seaography Example (Bakery) / Seaography Example (Sakila)
我们强烈推荐 Loco.rs。如果你在寻找开箱即用的框架,它提供了最接近 Rust 版「Ruby on Rails」的体验。
(Diesel 提供的示例有限,对 Web 框架的一手支持也有限)
事务 API
基于闭包的 API 与 begin() / commit() 接口(即 TransactionManager)各有优劣。SeaORM 两者都支持,但 Diesel actively 不鼓励你使用 begin() / commit() API。
因此,当你需要时可以这样做:
let txn = db.begin().await?;
{
let txn = txn.begin().await?;
{
let txn = txn.begin().await?;
// this will not be committed
}
// commit nested transaction
txn.commit().await?;
}
txn.commit().await?;
数据库后端泛型
SeaORM 的 Entity API 是后端泛型门面,在编译时不需要数据库后端。(事实上,编译实体时甚至不需要异步执行环境)。这意味着你可以设计一个复杂的应用,在生产环境使用 Postgres,但大量测试针对 SQLite 编写。
事实上,Zed Editor 的 collab API 服务器就是这样做的。
SeaORM 在合理的地方尝试提供跨数据库特性的抽象。初创公司可以构建数据库无关的产品。事实上,RisingWave 就是这样实现其数据存储以支持 Postgres、MySQL 和 SQLite 的。
Diesel 最近提供了一些编写多数据库泛型代码的能力,但你仍然需要自己处理跨数据库差异。但从根本上讲,在 SeaORM 中你只需编译一次:业务逻辑是一次编译的(即非特征基于),差异由后端的 match 语句处理。相比之下,Diesel 的代码必须为每个后端单独单态化。
基于 Entity 的关系模型
SeaORM 的关系建模在更高层次:我们将 M-N 关系视为原子单元,在许多场景下可以跳过中间表连接。
考虑我们的 readme 示例,
// SeaORM
let cake_with_filling: Vec<(cake::Model, Option<fruit::Model>)> =
Cake::find().find_also_related(Filling).all(db).await?;
在 Diesel 中要做到这一点,需要两次 join:
// Diesel
let cake_with_filling: Vec<(Cake, Filling)> =
cake::table
.inner_join(cake_filling::table.inner_join(filling::table))
.select((cake::all_columns, filling::all_columns))
.load::<(Cake, Filling)>(conn)?;
以下仅在 SeaORM 中可能:
// SeaORM
let cake_with_fillings: Vec<(cake::Model, Vec<filling::Model>)> = Cake::find()
.find_with_related(Filling) // two joins are performed
.all(db) // rows are consolidated by left entity
.await?;
反射
SeaORM 的模型和 ActiveModel 具有一些有用的反射能力。你可以在运行时动态获取/设置属性。
let mut fruit = fruit::Model { .. };
fruit.set("name".parse().unwrap(), "orange".into());
assert_eq!(fruit.name, "orange");
事实上,你可以轻松处理 JSON 输入:
let fruit = fruit::ActiveModel::from_json(json!({
"name": "Apple",
}))?;
assert_eq!(
fruit,
fruit::ActiveModel {
id: ActiveValue::NotSet,
name: ActiveValue::Set("Apple".to_owned()),
cake_id: ActiveValue::NotSet,
}
);
(在 Diesel 中无法进行任何反射,事实上,你必须自己编写结构体。)
内置工具
例如,SeaORM 开箱即用提供基于偏移和基于游标的分页,所以你不需要自己编写。
let paginator = Post::find()
.order_by_asc(post::Column::Id)
.paginate(db, posts_per_page);
let num_pages = paginator.num_pages().await?;
let posts: Vec<post::Model> = paginator.fetch_page(current_page).await?;
let mut cursor = post::Entity::find().cursor_by(post::Column::Id);
// Filter paginated result by "post"."id" > 1 AND "post"."id" < 100
cursor.after(1).before(100);
// Get first 10 rows (order by "post"."id" ASC)
let posts: Vec<post::Model> = cursor.first(10).all(db).await?
(Diesel 不提供此类工具。)
可以 Entity 优先
Entity 包含数据库架构的所有必要信息,所以你只需查看一个地方,就可以使用它们来设置数据库架构。
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "lineitem")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique_key = "item")]
pub order_id: i32,
#[sea_orm(unique_key = "item")]
pub cake_id: i32,
}
let stmts = Schema::new(backend).create_index_from_entity(lineitem::Entity);
assert_eq!(
backend.build(stmts[0]),
r#"CREATE UNIQUE INDEX "idx-lineitem-item" ON "lineitem" ("order_id", "cake_id")"#
);
(在 Diesel 中无法从 schema 描述反推回 DDL)
嵌套 select 及其他人体工程学
SeaORM 2.0 引入了许多 Diesel 中不可能实现的新特性。
use sea_orm::DerivePartialModel;
#[derive(DerivePartialModel)]
#[sea_orm(entity = "cake::Entity")]
struct CakeWithFruit {
id: i32,
name: String,
#[sea_orm(nested)]
fruit: Option<fruit::Model>,
}
let cakes: Vec<CakeWithFruit> = Cake::find()
.left_join(fruit::Entity) // no need to specify join condition
.into_partial_model() // only the columns in the partial model will be selected
.all(db)
.await?;
SeaORM:
#[derive(DerivePartialModel)]自动生成投影into_partial_model()仅选择需要的列- 连接可以从 entity 关系推断
Diesel:
- 你必须显式选择列
- 你必须显式将元组映射到你的结构体
- 没有内置的「局部模型」derive
人体工程学的 raw SQL
当查询复杂到需要手写 SQL 时,带参数展开的 raw SQL 是救星!
let item = Item { id: 2 }; // nested parameter access
let cake_ids = [2, 3, 4]; // expanded by the `..` operator
// can use nested select with raw SQL
let cake: Option<CakeWithBakery> = CakeWithBakery::find_by_statement(raw_sql!(
Sqlite,
r#"SELECT "cake"."name", "bakery"."name" AS "bakery_name"
FROM "cake"
LEFT JOIN "bakery" ON "cake"."bakery_id" = "bakery"."id"
WHERE "cake"."id" = {item.id} OR "cake"."id" IN ({..cake_ids})"#
))
.one(db)
.await?;
编译时意识
为什么 Diesel 中复杂的 schema 编译如此缓慢?以下是高层解释:
- Diesel 的
table!宏生成大量代码:每表一个模块、每列一个结构体、用于 join 的特征等。这提供了类型安全的查询构建器,但也意味着在底层生成了数千种不同的类型 - Diesel 将 SQL 查询编码到 Rust 类型中。这意味着你写的每个查询都会产生一个唯一的、深层嵌套的类型。类型推断和特征解析会变得非常重
- 所有东西都必须在一个大 crate 中,所以 rustc 无法有效并行编译
最糟糕的是:allow_tables_to_appear_in_same_query 具有 O(N^2) 复杂度
allow_tables_to_appear_in_same_query!(users, posts, comments);
展开为:
allow_tables_to_appear_in_same_query!(users, posts);
allow_tables_to_appear_in_same_query!(users, comments);
allow_tables_to_appear_in_same_query!(posts, comments);
如果你有 100 个实体,它会展开为 100 C 2 = 4950,即二项式展开。
SeaORM 设计为随复杂度良好扩展,并对增量编译友好。
- SeaQuery,底层查询构建器,是一个独立的 crate。虽然这是必需依赖,但它非常轻量,不重度泛型,编译快速
- SeaORM 为每个 entity 生成固定数量的结构体:
Entity、Model、ActiveModel、Column和PrimaryKey。无论列数多少,Column都是单个枚举 - SeaORM 随 schema 复杂度线性扩展:与实体数量及其关系成正比
- 如上所述,SeaORM 的 Entity 可以在纯 crate 中编译。如果你有非常复杂的 schema,可以简单地将它们拆分成多个 crate
社区采用
SeaORM 团队痴迷于改进文档、示例和生态系统集成。
我们很高兴看到公司和初创公司使用 SeaORM 构建的真实应用。以下按字母顺序排列:
事实上,SeaORM 在 crates.io 上有 25 万周下载量,其中大部分发生在工作日,表明 SeaORM 在专业场景中被大量使用。这一指标暗示 SeaORM 的用户活跃度可能高于 Diesel(请对此说法持保留态度)。






