关联对象保存模式
基本概念
在上一篇文章中,我们介绍了如何控制聚合根对象的保存模式。
本文将讨论如何控制关联对象的保存模式,关联对象有三种保存模式:
-
REPLACE (默认策略)
此策略包含两个方面的能力:
-
保存用户指定的关联对象。
首先,判断关联对象在数据库中是否已经存在。
-
如果关联对象的id属性被指定,按照id判断现有数据中是否存在相同对象。
-
否则按照关联对象的key属性判断。
对于不同的判断结果,给予不同的处理
-
如果关联对象已经存在,执行UPDATE操作
-
否则,执行INSET操作
-
-
脱钩用户未指定的其他关联对象。
如果某些关联对象在数据库中存在,但是在被保存的数据结构中并不存在,则对这些不再需要的关联对象进行脱钩操作。
脱钩操作受到其他配置 的影响,根据不同的配置,最终可能被执行的操作是报错、清空关联对象的外键、甚至删除关联对象。
脱钩操作并非本文讨论的重点,请参考相关章节
-
-
MERGE
和
REPLACE
相比,有关保存用户指定的关联对象的行为完全一样;然而,MERGE
操作并不会导致脱钩操作。 -
APPEND
和前两种模式差异较大,除了无条件地对关联对象进行INSERT外,无任何多余的操作。因此,自然也不需要关联对象的key配置。
本文根据难易程度,对讲解顺序做了调整:APPEND
, MERGE
, REPLACE
。
两种配置方法
Jimmer提供了一些快捷方法,它们可以快速配置关联保存模式,例如sqlClient.merge
, sqlClient.append
。这些Api很简单,用户一看就明白,本文不予讨论。
本文只讨论最原始的配置方法,存在两种配置方法。
-
配置特定的关 联属性
- Java
- Kotlin
BookStore store = ...任意数据结构,略...
sqlClient
.getEntities()
.saveCommand(store)
.setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.MERGE
)
.execute();val store: BookStore = ...任意数据结构,略...
sqlClient.save(store) {
setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.MERGE
)
}即,对于
BookStore
的关联属性books
而言,其关联对象 (类型为Book
) 的保存模式为MERGE
。对于其他属性而言,不受影响。
-
配置被保存数据结构的所有关联属性
- Java
- Kotlin
BookStore store = ...任意数据结构,略...
sqlClient
.getEntities()
.saveCommand(store)
.setAssociatedModeAll(AssociatedSaveMode.MERGE)
.execute();val store: BookStore = ...任意数据结构,略...
sqlClient.save(store) {
setAssociatedModeAll(AssociatedSaveMode.MERGE)
}对于当前被保存数据结构中的任何关联而言,关联对象的保存模式一律为
MERGE
。
针对特定关联的配置,优先于针对所有关联的配置。
这两种配置方法,唯一的区别在控制粒度的不同,功能层面不存在任何差异,因此,本文统一采用第一种配置方法。
1. APPEND
APPEND是最简单的模式,没有任何判断,无条件插入关联对象
- Java
- Kotlin
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();
val store = BookStore {
id = 2L
books().addBy {
name = "SQL in Action"
edition = 2
price = BigDecimal("59.9")
}
books().addBy {
name = "Redis in Action"
edition = 2
price = BigDecimal("49.9")
}
}
sqlClient.save(store) {
setMode(SaveMode.UPDATE_ONLY)
setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.APPEND
)
}
最终生成两条SQL语句
-
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* SQL in Action */, ? /* 2 */, ? /* 59.9 */, ? /* 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属性的对象,以展示这两种情况。
- Java
- Kotlin
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();
val store = BookStore {
id = 2L
books().addBy { // With id
id = 10L
name = "GraphQL in Action"
edition = 1
price = BigDecimal("59.9")
}
books().addBy { // Without id
name = "Redis in Action"
edition = 2
price = BigDecimal("49.9")
}
}
sqlClient.save(store) {
setMode(SaveMode.UPDATE_ONLY)
setAssociatedMode(
BookStoreProps.BOOKS,
AssociatedSaveMode.MERGE
)
}
生成如下SQL语句
-
基于
id
属性,判断第一个关联对象是否存在select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION
from BOOK tb_1_
where
tb_1_.ID = ? /* 10 */ -
假如上一步的判断结果指明对象存在,更新
update BOOK
set
NAME = ? /* GraphQL in Action */,
EDITION = ? /* 1 */,
PRICE = ? /* 59.9 */,
STORE_ID = ? /* 2 */
where
ID = ? /* 10 */ -
基于
key
(对于这里类型为Book
的关联对象而言,就是name
和edition
) 属性,判断第二个关联对象是否存在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 */ -
假如上一步的判断结果指明对象不存在,插入
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
)
values(
? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
)
3. REPLACE
在保存被用户指定的关联对象方面,REPLACE
和MERGE
并无区别。
然而,REPLACE
比MERGE
多了一个功能:脱钩操作。
如果某些关联对象在数据库中存在,但是在被保存的数据结构中并不存在,则对这些不再需要的关联对象进行脱钩操作。
下面这个例子和上一个例子唯一的区别在于并未改变关联对象保存模式,采用了默认行为REPLACE
。
- Java
- Kotlin
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();
val store = BookStore {
id = 2L
books().addBy { // With id
id = 10L
name = "GraphQL in Action"
edition = 1
price = BigDecimal("59.9")
}
books().addBy { // Without id
name = "Redis in Action"
edition = 2
price = BigDecimal("49.9")
}
}
sqlClient.save(store) {
setMode(SaveMode.UPDATE_ONLY)
// There is no need to call `setAssociatedSaveMode`,
// because `REPLACE` is the default behavior
}
生成如下SQL语句
-
和上个例子相同,可忽略
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION
from BOOK tb_1_
where
tb_1_.ID = ? /* 10 */ -
和上个例子相同,可忽略
update BOOK
set
NAME = ? /* GraphQL in Action */,
EDITION = ? /* 1 */,
PRICE = ? /* 59.9 */,
STORE_ID = ? /* 2 */
where
ID = ? /* 10 */ -
和上个例子相同,可忽略
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 */ -
和上个例子相同,可忽略
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
)
values(
? /* Redis in Action */, ? /* 2 */, ? /* 49.9 */, ? /* 2 */
) -
脱钩操作步骤之一
被保存的
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 */
) -
脱钩操作步骤之一
脱钩操作受到其他配置的影响,不同的配置会导致不同的行为。本文不深入讨论这个问题,仅展示一种可能性
delete from BOOK_AUTHOR_MAPPING
where
BOOK_ID in (
? /* 11 */, ? /* 12 */
) -
脱钩操作步骤之一
脱钩操作受到其他配置的影响,不同的配置会导致不同的行为。本文不深入讨论这个问题,仅展示一种可能性
delete from BOOK
where
ID in (
? /* 11 */, ? /* 12 */
)
脱钩操作受到其他配置的影响,根据不同的配置,最终可能报错、清空关联对象外键、甚至删除关联对象。
脱钩操作并非本文讨论的重点,请参考相关章节
总结
按照之前的论述,保存指令的本质是将用户要保存的数据结构和数据库中现有的数据结构做对比, 对有变化的局部进行同步,
。不难发现,默认的REPLACE
模式和此图契合。然而,MERGE
和APPEND
是削弱后变种。是的,它们是客观存在的需求,有的时候,开发人员面对更简单的场景,需要执行更简单的操作。