跳到主要内容
版本:2.0.x

嵌套 ActiveModel

在 SeaORM 2.0 中,我们引入了 Smart Entity Loader,使查询多路径关系到嵌套模型变得简单高效。这解决了读取端的问题。

有了 nested ActiveModel,你现在可以做相反的事:在一次操作中将嵌套对象回写数据库。SeaORM 遍历树、检测变更、构建 insert 和 update 语句,并以正确的顺序执行它们以遵守外键依赖。

你可以在此页面的 快速入门示例中找到本文描述的所有技术。

概要

以下操作以原子方式将新的 user + profile + post + tag + post_tag 集合保存到数据库:

let user = user::ActiveModel::builder()
.set_name("Bob")
.set_email("bob@sea-ql.org")
.set_profile(profile::ActiveModel::builder().set_picture("image.jpg"))
.add_post(
post::ActiveModel::builder()
.set_title("Nice weather")
.add_tag(tag::ActiveModel::builder().set_tag("sunny")),
)
.save(db)
.await?;

展开

此 builder 模式构建以下对象树:

user::ActiveModelEx {
name: Set("Bob".into()),
email: Set("bob@sea-ql.org".into()),
profile: HasOneModel::Set(profile::ActiveModelEx {
picture: Set("image.jpg".into()),
..Default::default()
}),
posts: HasManyModel::Append(post::ActiveModelEx {
title: Set("Nice weather".into()),
tags: HasManyModel::Append(tag::ActiveModel {
tag: Set("sunny".into()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
}
.save(db)
.await?

.. 等于手动执行以下操作:

let txn = db.begin().await?;

// insert a user
let user = user::ActiveModelEx {
name: Set("Bob".into()),
email: Set("bob@sea-ql.org".into()),
..Default::default()
}.insert(&txn).await?;

// profile belongs_to user (1-1)
let profile = profile::ActiveModelEx {
user_id: Set(user.id),
picture: Set("image.jpg".into()),
..Default::default()
}.insert(&txn).await?;

// post belongs_to user (1-N)
let post = post::ActiveModelEx {
user_id: Set(user.id),
title: Set("Nice weather".into()),
..Default::default()
}.insert(&txn).await?;

// insert a tag
let tag = tag::ActiveModel {
tag: Set("sunny".into()),
..Default::default()
}
.insert(&txn)
.await?;

// associate tag to post
post_tag::ActiveModel {
post_id: Set(post.id),
tag_id: Set(tag.id),
}
.insert(&txn)
.await?;

txn.commit().await?;

关系依赖

问题的核心在于弄清 Entity 之间的外键关系并以正确的顺序执行查询。SeaORM 支持以下关系:

拥有一 / 属于

User 1-1 Profile

user.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(has_one)]
pub profile: HasOne<super::profile::Entity>,
..
}

user_id 上有 unique 键,使此关系为一对一。

profile.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "profile")]
pub struct Model {
#[sea_orm(unique)]
pub user_id: i32,
#[sea_orm(belongs_to, from = "user_id", to = "id")]
pub user: HasOne<super::user::Entity>,
..
}

由于 profile 属于 user,必须先 insert user 才能获得其 id

在 SeaORM 中,无论模型如何嵌套,都会以正确的顺序执行。

// also okay:
profile::ActiveModel::builder()
.set_user(
user::ActiveModel::builder()
.set_name("Alice")
.set_email("alice@rust-lang.org"),
)
.set_picture("image.jpg")
.save(db)
.await?;

拥有多

User 1-N Post

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "post")]
pub struct Model {
pub user_id: i32,
#[sea_orm(belongs_to, from = "user_id", to = "id")]
pub author: HasOne<super::user::Entity>,
..
}

这与 1-1 非常相似,嵌套模型是向量而不是 option。

有两种可能的操作:ReplaceAppend。默认操作是 append,它是非破坏性的。

假设 Bob 写了一篇新博客文章,没有理由需要查询 Bob 写的所有文章;我们可以简单地执行以下操作:

// query user, but no posts
let bob = user::Entity::load().filter_by_email("bob@sea-ql.org").one(db).await?.unwrap();

let mut bob.into_active_model();
bob.posts.push(post::ActiveModel::builder().set_title("Another weekend"));
bob.save(db).await?; // INSERT INTO post ..

但是,有时我们确实希望空向量表示“删除全部”,或者我们希望指定精确的子集。然后我们可以使用 Replace

bob.posts.replace_all([]); // delete all
bob.posts.replace_all([post_1]); // retain only this post

这将导致以下操作,其中除 post 1 之外的文章将被删除:

SELECT FROM post WHERE user_id = bob.id
DELETE FROM post WHERE id = 2

多对多

Post M-N Tag

多对多关系在建模复杂 schema 时至关重要。SeaORM 的一个独特之处在于它将多对多关系建模为一等构造,因此你无需考虑 junction table。

post.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
#[sea_orm(has_many, via = "post_tag")] // ⬅ specify junction table
pub tags: HasMany<super::tag::Entity>,
..
}
post_tag.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "post_tag")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)] // ⬅ composite key
pub post_id: i32,
#[sea_orm(primary_key, auto_increment = false)] // ⬅ composite key
pub tag_id: i32,
#[sea_orm(belongs_to, from = "post_id", to = "id")] // ⬅ belongs to both
pub post: Option<super::post::Entity>,
#[sea_orm(belongs_to, from = "tag_id", to = "id")] // ⬅ belongs to both
pub tag: Option<super::tag::Entity>,
}
tag.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "tag")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub tag: String,
}

M-N 关系不仅仅是 1-N + 1-1,它实际上脱离了该概念。让我们看以下示例,insert 一篇带 2 个 tag 的新 post:

// Insert one tag for later use
let sunny = tag::ActiveModel::builder().set_tag("sunny").save(db).await?;

// Insert a new post with 2 tags
let post = post::ActiveModel::builder()
.set_title("A sunny day")
.set_user(bob)
.add_tag(sunny) // an existing tag
.add_tag(tag::ActiveModel::builder().set_tag("outdoor")) // a new tag
.save(db) // new tag will be created and associcated to the new post
.await?;

这将导致以下操作:

INSERT INTO post (user_id, title) VALUES (bob.id, 'A sunny day') RETURNING id
INSERT INTO tag (tag) VALUES ('outdoor') RETURNING id
INSERT INTO post_tag (post_id, tag_id) VALUES (post.id, sunny.id) (post.id, outdoor.id)

它们的关系不再是“belongs to”,它们只是相互关联。 从 post 中移除 tag 不会删除 tag,只会删除 junction table 中的关联。

post.tags.replace_all([outdoor]); // let's say we remove the tag sunny

结果是:

DELETE FROM post_tag WHERE (post_id, tag_id) IN ((post.id, sunny.id))

再举一个例子以便理解:在 Film M-N Actor 关系中,删除一部电影不会删除其演员,因为他们仍可能出现在其他电影中。

注意

SeaORM 也支持在 junction table 上使用代理主键,但推荐使用复合主键。

film_actor.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "film_actor")]
pub struct Model {
#[sea_orm(primary_key)] // ⬅ normal primary key
pub id: i32,
#[sea_orm(unique_key = "film_actor")] // ⬅ unique key
pub film_id: i32,
#[sea_orm(unique_key = "film_actor")] // ⬅ unique key
pub actor_id: i32,
#[sea_orm(belongs_to, from = "film_id", to = "id")]
pub film: HasOne<super::film::Entity>,
#[sea_orm(belongs_to, from = "actor_id", to = "id")]
pub actor: HasOne<super::actor::Entity>,
}

变更检测

让我们回到基础,在 SeaORM 中每个模型都由 ActiveModel 支持:

post.rs
pub struct ModelEx {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: i32,
pub title: String,
pub author: HasOne<super::user::Entity>,
pub tags: HasMany<super::tag::Entity>,
}

// generated by macro:
pub struct ActiveModelEx {
#[sea_orm(primary_key)]
pub id: ActiveValue<i32>,
pub user_id: ActiveValue<i32>,
pub title: ActiveValue<String>,
pub author: HasOneModel<super::user::Entity>,
pub tags: HasManyModel<super::tag::Entity>,
}

每个 ActiveValue 都是三态的。

pub enum ActiveValue<V>
{
Set(V),
Unchanged(V),
NotSet,
}

这允许 SeaORM 跟踪变更。当你首次从数据库查询新的模型时,默认状态是 Unchanged。当你在代码中进行某些更改时,状态将变为 Set。因此当你运行 save 时,只有更改的列会被更新。

let post = post::Entity::find_by_id(22).one(db).await?.unwrap(); // Model
let mut post = post.into_active_model(); // ActiveModel
post.title = Set("The weather changed!");
post.save(db).await?; // UPDATE post SET title = '..' WHERE id = 22

这有两个优点:避免过度更新,减少通过网络发送的数据量。更重要的是,多个 API 端点可以安全地更新不同的列集,而不会有竞态条件的风险,也无需依赖 transaction 或锁定机制。

这一概念扩展到 nested ActiveModel,允许 SeaORM 遍历嵌套对象树并确定哪些子树已更改需要更新。

例如:

let mut bob: user::ActiveModel = ..;

// update post title
bob.posts[0].title = Set("Lorem ipsum dolor sit amet".into());
// update post comment
bob.posts[0].comments[0].comment = Set("nice post! I learnt a lot".into());
// add a new comment too
bob.posts[1].comments.push(
comment::ActiveModel::builder().set_comment("interesting!")
);
bob.save(db).await?;

结果是:

BEGIN TRANSACTION

UPDATE post SET title = '..' WHERE id = post.id
UPDATE comment SET comment = '..' WHERE id = comment.id
INSERT INTO comment (post_id, comment) VALUES (post.id. '..')

COMMIT

内容很多,但一旦你建立了对 SeaORM 概念和机制的清晰心智模型,你会发现自己的效率大大提高!

级联删除

如果关系使用 ON DELETE CASCADE 定义,则不存在此问题。但是,SeaORM 也可以在客户端执行 cascade delete。它应用上述相同的规则,但方向相反。

例如,Post 属于 User。必须先删除所有 post 才能删除 user;否则,SQL 操作将失败。

let user_4 = user::Entity::find_by_id(4).one(db).await?.unwrap();

user_4.cascade_delete(db).await?; // equivalent to below
user_4.into_ex().delete(db).await?;
-- query the profile belonging to user
SELECT FROM profile INNER JOIN user ON user.id = profile.user_id WHERE user.id = 4 LIMIT 1
-- delete the profile, if exist
DELETE FROM profile WHERE profile.id = 2
-- query the posts belonging to user
SELECT FROM post INNER JOIN user ON user.id = post.user_id WHERE user.id = 4
-- query the comments belonging to post
SELECT FROM comment INNER JOIN post ON post.id = comment.post_id WHERE post.id = 7
-- delete the comments, if exist
DELETE FROM comment WHERE comment.id = 5
-- query the post's tags
SELECT FROM post_tag INNER JOIN post ON post_tag.post_id = post.id WHERE post.id = 7
-- delete the post-tag associations
DELETE FROM post_tag WHERE (post_id, tag_id) IN ((7, 2), (7, 3), (7, 4))
-- post has no dependents, safe to delete now
DELETE FROM post WHERE post.id = 7
-- user has no dependents, safe to delete now
DELETE FROM user WHERE user.id = 4

弱属于

还有一种尚未提及的 Belongs To 关系的特殊情况:弱 1-N 关联,其中 Entity 可能有所有者,但不是严格必需的。

举例来说,Post 1-N Attachment。但用户可以在起草 post 之前上传 attachment,因此某些 attachment 可能没有关联的 post。

attachment.rs
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "attachment")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub post_id: Option<i32>, // this is nullable
pub file: String,
#[sea_orm(belongs_to, from = "post_id", to = "id")]
pub post: HasOne<super::post::Entity>,
}
// this post has attachment_2
let post_1 = post::Entity::find_by_id(1).one(db).await?.unwrap();
post_1.cascade_delete(db).await?;

// post_id of attachment will be set to null, instead of deleting the attachment
let attachment_2 = attachment::Entity::find_by_id(2).one(db).await?.unwrap();
assert!(attachment_2.post_id.is_none());

幂等性

一般规则是幂等性:第二次保存 ActiveModel 应该是空操作。 除非你使用 replacedelete,否则不会执行 delete。

let post = post.save(db).await?;
let post = post.save(db).await?; // no-op, as all fields are unchanged

你提供的 ActiveModel 是数据期望最终状态的快照,SeaORM 应确保最终达到该状态。这可能很复杂,如有 bug 请报告。

提示

跟踪何时使用 insertupdate 可能很困难,除非预期操作是“从头创建新记录”。默认使用 save,让 SeaORM 决定何时执行 insertupdate

(如果你已经觉得这些概念很熟悉,难怪它被称为 ActiveModel。)

向后兼容性

本文介绍的 2.0 所有功能与 1.0 完全向后兼容,因为只添加了新类型和方法:ActiveModelExHasOneModelHasManyModel 和一些方法。ActiveModel 的行为与之前完全相同。

但是,由于宏需要关系的属性来生成实现,这些功能仅对 #[sea_orm::model] 可用,对 #[sea_orm::compact_model] 不可用。