跳到主要内容

关联对象保存模式

基本概念

上一篇文章中,我们介绍了如何控制聚合根对象的保存模式。

本文将讨论如何控制关联对象的保存模式,关联对象有三种保存模式:

  • REPLACE (默认策略)

    此策略包含两个方面的能力:

    1. 保存用户指定的关联对象。

      首先,判断关联对象在数据库中是否已经存在。

      • 如果关联对象的id属性被指定,按照id判断现有数据中是否存在相同对象。

      • 否则按照关联对象的key属性判断。

      对于不同的判断结果,给予不同的处理

      • 如果关联对象已经存在,执行UPDATE操作

      • 否则,执行INSET操作

    2. 脱钩用户未指定的其他关联对象。

      如果某些关联对象在数据库中存在,但是在被保存的数据结构中并不存在,则对这些不再需要的关联对象进行脱钩操作。

      脱钩操作受到其他配置的影响,根据不同的配置,最终可能被执行的操作是报错、清空关联对象的外键、甚至删除关联对象。

      脱钩操作并非本文讨论的重点,请参考相关章节

  • MERGE

    REPLACE相比,有关保存用户指定的关联对象的行为完全一样;然而,MERGE操作并不会导致脱钩操作

  • APPEND

    和前两种模式差异较大,除了无条件地对关联对象进行INSERT外,无任何多余的操作。因此,自然也不需要关联对象的key配置。

警告

本文根据难易程度,对讲解顺序做了调整:APPEND, MERGE, REPLACE

两种配置方法

Jimmer提供了一些快捷方法,它们可以快速配置关联保存模式,例如sqlClient.merge, sqlClient.append。这些Api很简单,用户一看就明白,本文不予讨论。

本文只讨论最原始的配置方法,存在两种配置方法。

  • 配置特定的关联属性

    BookStore store = ...任意数据结构,略...

    sqlClient
    .getEntities()
    .saveCommand(store)
    .setAssociatedMode(
    BookStoreProps.BOOKS,
    AssociatedSaveMode.MERGE
    )
    .execute();

    即,对于BookStore的关联属性books而言,其关联对象 (类型为Book) 的保存模式为MERGE

    对于其他属性而言,不受影响。

  • 配置被保存数据结构的所有关联属性

    BookStore store = ...任意数据结构,略...

    sqlClient
    .getEntities()
    .saveCommand(store)
    .setAssociatedModeAll(AssociatedSaveMode.MERGE)
    .execute();

    对于当前被保存数据结构中的任何关联而言,关联对象的保存模式一律为MERGE

提示

针对特定关联的配置,优先于针对所有关联的配置。

这两种配置方法,唯一的区别在控制粒度的不同,功能层面不存在任何差异,因此,本文统一采用第一种配置方法。

1. APPEND

APPEND是最简单的模式,没有任何判断,无条件插入关联对象

BookStore store = BookStoreDraft.$.produce(draft -> {
draft.setId(2L);
draft.addIntoBooks(book -> {
book.setName("SQL in Action");
book.setEdition(2);
book.setPrice(new BigDecimal("59.9"));
});
draft.addIntoBooks(book -> {
book.setName("Redis in Action");
book.setEdition(2);
book.setPrice(new BigDecimal("49.9"));
});
});
sqlClient
.getEntities()
.saveCommand(store)
.setMode(SaveMode.UPDATE_ONLY)
.setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.APPEND
)
.execute();

最终生成两条SQL语句

  1. insert into BOOK(
    NAME, EDITION, PRICE, STORE_ID
    ) values(
    ? /* SQL in Action */, ? /* 2 */, ? /* 59.9 */, ? /* 2 */
    )
  2. insert into BOOK(
    NAME, EDITION, PRICE, STORE_ID
    ) values(
    ? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
    )

2. MERGE

APPEND不同,MERGE不会无条件插入关联对象;它会判断关联对象是否存在,从而决定应该执行update还是insert操作。

  • 如果关联对象的id属性被指定,按照id判断现有数据中是否存在相同对象。

  • 否则,按照关联对象的key属性判断。

备注

在接下来的例子中,我们让关联集合BookStore.books既包含有id的对象也包含没有id属性的对象,以展示这两种情况。

BookStore store = BookStoreDraft.$.produce(draft -> {
draft.setId(2L);
draft.addIntoBooks(book -> { // With id
book.setId(10L);
book.setName("GraphQL in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("59.9"));
});
draft.addIntoBooks(book -> { // Without id
book.setName("Redis in Action");
book.setEdition(2);
book.setPrice(new BigDecimal("49.9"));
});
});
sqlClient
.getEntities()
.saveCommand(store)
.setMode(SaveMode.UPDATE_ONLY)
.setAssociatedMode(
BookStoreProps.BOOKS,
// highlight-nex-line
AssociatedSaveMode.MERGE
)
.execute();

生成如下SQL语句

  1. 基于id属性,判断第一个关联对象是否存在

    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION
    from BOOK tb_1_
    where
    tb_1_.ID = ? /* 10 */
  2. 假如上一步的判断结果指明对象存在,更新

    update BOOK
    set
    NAME = ? /* GraphQL in Action */,
    EDITION = ? /* 1 */,
    PRICE = ? /* 59.9 */,
    STORE_ID = ? /* 2 */
    where
    ID = ? /* 10 */
  3. 基于key (对于这里类型为Book的关联对象而言,就是nameedition) 属性,判断第二个关联对象是否存在

    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION
    from BOOK tb_1_
    where
    tb_1_.NAME = ? /* Redis in Action */
    and
    tb_1_.EDITION = ? /* 2 */
  4. 假如上一步的判断结果指明对象不存在,插入

    insert into BOOK(
    NAME, EDITION, PRICE, STORE_ID
    )
    values(
    ? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
    )

3. REPLACE

在保存被用户指定的关联对象方面,REPLACEMERGE并无区别。

然而,REPLACEMERGE多了一个功能:脱钩操作

如果某些关联对象在数据库中存在,但是在被保存的数据结构中并不存在,则对这些不再需要的关联对象进行脱钩操作。

备注

下面这个例子和上一个例子唯一的区别在于并未改变关联对象保存模式,采用了默认行为REPLACE

BookStore store = BookStoreDraft.$.produce(draft -> {
draft.setId(2L);
draft.addIntoBooks(book -> { // With id
book.setId(10L);
book.setName("GraphQL in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("59.9"));
});
draft.addIntoBooks(book -> { // Without id
book.setName("Redis in Action");
book.setEdition(2);
book.setPrice(new BigDecimal("49.9"));
});
});
sqlClient
.getEntities()
.saveCommand(store)
.setMode(SaveMode.UPDATE_ONLY)
// There is no need to call `setAssociatedSaveMode`,
// because `REPLACE` is the default behavior
.execute();

生成如下SQL语句

  1. 和上个例子相同,可忽略

    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION
    from BOOK tb_1_
    where
    tb_1_.ID = ? /* 10 */
  2. 和上个例子相同,可忽略

    update BOOK
    set
    NAME = ? /* GraphQL in Action */,
    EDITION = ? /* 1 */,
    PRICE = ? /* 59.9 */,
    STORE_ID = ? /* 2 */
    where
    ID = ? /* 10 */
  3. 和上个例子相同,可忽略

    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION
    from BOOK tb_1_
    where
    tb_1_.NAME = ? /* Redis in Action */
    and
    tb_1_.EDITION = ? /* 2 */
  4. 和上个例子相同,可忽略

    insert into BOOK(
    NAME, EDITION, PRICE, STORE_ID
    )
    values(
    ? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
    )
  5. 脱钩操作步骤之一

    被保存的BookStore对象的关联集合属性books包含两个Book类型的关联对象, 它们的id分别为10 (之前被修改的那个对象) 和100 (之前被插入的那个对象,假设自动编号分配结果为100)

    那么, 除了它们之外,当前BookStore对象在数据库中是是否存在其他Book对象需要被脱钩?

    select
    ID
    from BOOK
    where
    STORE_ID = ? /* 2 */
    and
    // highlight-next-line
    ID not in ( // Be careful, this is `not in`
    ? /* 10 */, ? /* 100 */
    )
  6. 脱钩操作步骤之一

    脱钩操作受到其他配置的影响,不同的配置会导致不同的行为。本文不深入讨论这个问题,仅展示一种可能性

    delete from BOOK_AUTHOR_MAPPING
    where
    BOOK_ID in (
    ? /* 11 */, ? /* 12 */
    )
  7. 脱钩操作步骤之一

    脱钩操作受到其他配置的影响,不同的配置会导致不同的行为。本文不深入讨论这个问题,仅展示一种可能性

    delete from BOOK
    where
    ID in (
    ? /* 11 */, ? /* 12 */
    )
信息

脱钩操作受到其他配置的影响,根据不同的配置,最终可能报错、清空关联对象外键、甚至删除关联对象。

脱钩操作并非本文讨论的重点,请参考相关章节

总结

按照之前的论述,保存指令的本质是将用户要保存的数据结构和数据库中现有的数据结构做对比, 对有变化的局部进行同步,

不难发现,默认的REPLACE模式和此图契合。然而,MERGEAPPEND是削弱后变种。是的,它们是客观存在的需求,有的时候,开发人员面对更简单的场景,需要执行更简单的操作。