脱钩操作
在采用关联模式AssociatedSaveMode.REPLACE保存关联对象时,会涉及一个重要的概念:脱勾操作。
概念
| 数据库已有数据结构 | 用户期望保存的数据结构 |
|---|---|
| |
-
对于
Bread而言,在新旧数据结构中都存在,对应update操作 -
对于
Drinks而言,在旧数据结构中不存在,但在新数据中存在,对应INSERT操作 -
对于
Meat而言,在旧数据结构中存在,但在新数据中不存在,对应的操作叫做脱勾操作。
脱勾针对两种关联
-
中间表关联
所谓中间表的关联,显然,指的就是本教程中
Book.authors和Author.books这类关联。其脱钩操作非常简单,只是简单地删除中间表的关联而已,关联对象本身不受任何影响。
-
子表关联
所谓子表关联,指基于外键 (无论真伪) 关联的逆关联,例如本教程中的
BookStore.books。其脱钩操作非常简单相对复杂。开发人员可以通过配置来控制具体脱钩行为。
接下来,我们讨论这两种脱勾操作。
脱勾中间表关联
在介绍关联对象的REPLACE保存模式时,我们已经展示了这种最简单的脱钩行为。
因此,本文不再赘述。
脱勾子表关联
所谓子表关联,指基于外键 (无论真伪) 关联的逆向关联,例如BookStore.books。
其脱钩操作非常简单相对复杂。开发人员可以为外键所对应的属性 (例如这里的Book.store) 配置脱钩模式,来实现不同的脱钩行为。
脱勾模式
子对象脱勾操作有5种模式
| 模式 | 描述 |
|---|---|
NONE (默认) | 视全局配置jimmer.default-dissociate-action-checking而定
|
| LAX | 脱钩操作不执行任何动作。
|
| CHECK | 不支持脱钩操作,如果数据库中当前父对象拥有需要脱钩的子对象,则抛出异常阻止操作。 |
| SET_NULL | 把被脱勾的子对象的外键设置为null。使用此模式的前提是子对象的外键关联属性是nullnullable的;否则尝试此配置将会导致异常。 |
| DELETE | 将被脱勾的子对象删除。 |
配置脱勾模式有两种方法
-
在实体上用注解静态配置,静态配置是全局的。
-
在代码中代码动态配置,动态配置可以覆盖静态配置,但只影响当前保存指令。
以SET_NULL为例
-
基于实体的静态配置 (大部分情况下的选择,供绝大部分业务使用)
- Java
- Kotlin
Book.java@Entity
public interface Book {
@OnDissociate(DissociateAction.SET_NULL)
@Nullable
@ManyToOne
BookStore store();
...省略其他代码...
}Book.kt@Entity
interface Book {
@OnDissociate(DissociateAction.SET_NULL)
@ManyToOne
val store: BookStore?
...省略其他代码...
} -
基于保存指令的动态配置 (仅针对单条保存指令,极少数有特殊需求的业务使用)
- Java
- Kotlin
sqlClient
.getEntities()
.saveCommand(book)
.setDissociateAction(
BookProps.STORE,
DissociateAction.SET_NULL
)
.execute();sqlClient.save(book) {
setDissociateAction(
Book::store,
DissociateAction.SET_NULL
)
}
保存代码
接下来,我们通过保存包含一对多关联BookStore.books的数据结构来讲解各种脱钩模式。
编写保存代码如下
- Java
- Kotlin
List<BookStore> stores = Arrays.asList(
Immutables.createBookStore(draft -> {
draft.setName("O'REILLY");
draft.addIntoBooks(book -> {
book.setName("Learning GraphQL");
book.setEdition(3);
book.setPrice(new BigDecimal("51.9"));
});
draft.addIntoBooks(book -> {
book.setName("Learning GraphQL");
book.setEdition(4);
book.setPrice(new BigDecimal("43.9"));
});
draft.addIntoBooks(book -> {
book.setName("Effective TypeScript");
book.setEdition(3);
book.setPrice(new BigDecimal("88.9"));
});
draft.addIntoBooks(book -> {
book.setName("Effective TypeScript");
book.setEdition(4);
book.setPrice(new BigDecimal("85.9"));
});
draft.addIntoBooks(book -> {
book.setName("Programming TypeScript");
book.setEdition(3);
book.setPrice(new BigDecimal("48.9"));
});
draft.addIntoBooks(book -> {
book.setName("Programming TypeScript");
book.setEdition(4);
book.setPrice(new BigDecimal("47.9"));
});
}),
Immutables.createBookStore(draft -> {
draft.setName("MANNING");
draft.addIntoBooks(book -> {
book.setName("GraphQL in Action");
book.setEdition(3);
book.setPrice(new BigDecimal("80.9"));
});
draft.addIntoBooks(book -> {
book.setName("GraphQL in Action");
book.setEdition(4);
book.setPrice(new BigDecimal("81.9"));
});
})
);
sqlClient.saveEntities(
stores,
// 也可以不指定次参数,对`save`方法而言,
// AssociatedSaveMode默认为REPLACE
AssociatedSaveMode.REPLACE
);
val stores = listOf(
BookStore {
name = "O'REILLY"
books().addBy {
name = "Learning GraphQL"
edition = 3
price = BigDecimal("51.9")
}
books().addBy {
name = "Learning GraphQL"
edition = 4
price = BigDecimal("32.9")
}
books().addBy {
name = "Effective TypeScript"
edition = 3
price = BigDecimal("88.9")
}
books().addBy {
name = "Effective TypeScript"
edition = 4
price = BigDecimal("85.9")
}
books().addBy {
name = "Programming TypeScript"
edition = 3
price = BigDecimal("48.9")
}
books().addBy {
name = "Programming TypeScript"
edition = 4
price = BigDecimal("47.9")
}
},
BookStore {
name = "MANNING"
books().addBy {
name = "GraphQL in Action"
edition = 3
price = BigDecimal("80.9")
}
books().addBy {
name = "GraphQL in Action"
edition = 4
price = BigDecimal("81.9")
}
}
)
sqlClient.saveEntities(
stores,
// 也可以 不指定次参数,对`save`方法而言,
// AssociatedSaveMode默认为REPLACE
AssociatedSaveMode.REPLACE
)
新旧对象对比如下
| 数据库现有数据 | 用户期望保存的数据 |
|---|---|
| |
| |
其中,不再需的8个关联对象,即,需要被脱钩的8个对象,被高亮显示。
那么,Jimmer会如何脱钩这些关联对象呢?
其实,不同的脱钩配置会导致不同的脱钩行为。
1. NONE (默认)
NONE的行为并不是固化的,而是视全局配置jimmer.default-dissociate-action-check而定:
-
如果
jimmer.default-dissociate-action-check为true (默认) 或 当前关联所基于的外键是真的 (数据库中存在相应的外键约束,请参见真假外键),视为CHECK。 -
如果
jimmer.default-dissociate-action-check为false且当前关联所基于的外键是假的 (数据库中没有相应的外键约束,请参见真假外键),视为LAX。
2. LAX
-
脱钩模式
LAX表示脱钩操作什么也不做,。 -
关联保存模式
AssociatedSaveMode.REPLACE表示不再需要的关联关系需要被丢弃。
很明显,两个规则发生了冲突。此时,AssociatedSaveMode.REPLACE更为优先,LAX被无视,最终被视为CHECK。
所以,无法通过本文的例子演示LAX。如果要了解LAX的作用,请参见删除指令。
3. CHECK
CHECK模式不允许脱钩关联对象。Jimmer会查询是否存在需要被脱钩的关联对象,如果存在,则抛出异常。
脱钩模式的设置,既可以通过实体定义来设置,也可以通过保存指令来设置。
这里,我们通过实体配置来设置Book.store的脱钩模式
- Java
- Kotlin
@Entity
@KeyUniqueConstraint(noMoreUniqueConstraints = true)
public interface Book {
@OnDissociate(DissociateAction.CHECK)
@Nullable
@ManyToOne
BookStore store();
...省略其他代码...
}
@Entity
@KeyUniqueConstraint(noMoreUniqueConstraints = true)
interface Book {
@OnDissociate(DissociateAction.CHECK)
@ManyToOne
val store: BookStore?
...省略其他代码...
}
为 了演示更简单的SQL生成,我们假设sqlClient的targetTransferable功能被打开,这是之前已经介绍过的内容,请参见这里
运行前文的保存代码,最终会生成三条SQL
-
保存根对象并非本文的讨论重点,默认折叠
- H2
- Mysql
- Postgres
merge into BOOK_STORE(
NAME
) key(NAME) values(
?
)
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
insert into BOOK_STORE(
NAME
) values(
? /* MANNING */
) on duplicate key update
/* fake update to return all ids */ ID = last_insert_id(ID) -
insert into BOOK_STORE(
NAME
) values(
? /* AMAZON */
) on duplicate key update
/* fake update to return all ids */ ID = last_insert_id(ID)
insert into BOOK_STORE(
NAME
) values(
?
) on conflict(
NAME
) do update set
/* fake update to return all ids */ NAME = execluded.NAME
returning ID
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */- 假设
MANNING存在,现有id为2 - 假设
AMAZON存在,插入后,数据库自动编号新分配的id为100
-
保存关联及关联对象也并非本文的讨论重点,默认折叠
- H2
- Mysql
- Postgres
merge into BOOK(
NAME, EDITION, PRICE, STORE_ID
) key(
NAME, EDITION
) values(
?, ?, ?, ?
)
/* batch-0: [Learning GraphQL, 3, 51.9, 1] */
/* batch-1: [Learning GraphQL, 4, 43.9, 1] */
/* batch-2: [Effective TypeScript, 3, 88.9, 1] */
/* batch-3: [Effective TypeScript, 4, 85.9, 1] */
/* batch-4: [Programming TypeScript, 3, 48.9, 1] */
/* batch-5: [Programming TypeScript, 4, 47.9, 1] */
/* batch-6: [GraphQL in Action, 3, 80.9, 2] */
/* batch-7: [GraphQL in Action, 4, 81.9, 2] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* Learning GraphQL */,
? /* 3 */,
? /* 51.9 */,
? /* 1 */
) on duplicate key udpate
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* Learning GraphQL */,
? /* 4 */,
? /* 43.9 */,
? /* 1 */
) on duplicate key udpate
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* Effective TypeScript */,
? /* 3 */,
? /* 88.9 */,
? /* 1 */
) on duplicate key udpate
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* Effective TypeScript */,
? /* 4 */,
? /* 85.9 */,
? /* 1 */
) on duplicate key udpate
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* Programming TypeScript */,
? /* 3 */,
? /* 48.9 */,
? /* 1 */
) on duplicate key udpate
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* Programming TypeScript */,
? /* 4 */,
? /* 47.9 */,
? /* 1 */
) on duplicate key udpate
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* GraphQL in Action */,
? /* 3 */,
? /* 80.9 */,
? /* 2 */
) on duplicate key udpate
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID) -
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* GraphQL in Action */,
? /* 4 */,
? /* 81.9 */,
? /* 2 */
) on duplicate key udpate
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID)
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
?, ?, ?, ?
) on conflict(
NAME, EDITION
) do update set
PRICE = excluded.PRICE,
STORE_ID = excluded.STORE_ID
returning ID
/* batch-0: [Learning GraphQL, 3, 51.9, 1] */
/* batch-1: [Learning GraphQL, 4, 43.9, 1] */
/* batch-2: [Effective TypeScript, 3, 88.9, 1] */
/* batch-3: [Effective TypeScript, 4, 85.9, 1] */
/* batch-4: [Programming TypeScript, 3, 48.9, 1] */
/* batch-5: [Programming TypeScript, 4, 47.9, 1] */
/* batch-6: [GraphQL in Action, 3, 80.9, 2] */
/* batch-7: [GraphQL in Action, 4, 81.9, 2] */ -
查询是否有需要脱钩的对象 (如果有,抛出异常组织保持指令)
select
tb_1_.ID
from BOOK tb_1_
where
tb_1_.STORE_ID in (
? /* 1 */, ? /* 2 */
)
and
(tb_1_.STORE_ID, tb_1_.ID) not in (
(? /* 1 */, ? /* 3 */),
(? /* 1 */, ? /* 100 */),
(? /* 1 */, ? /* 6 */),
(? /* 1 */, ? /* 101 */),
(? /* 1 */, ? /* 9 */),
(? /* 1 */, ? /* 102 */),
(? /* 2 */, ? /* 12 */),
(? /* 2 */, ? /* 103 */)
)
limit ? /* 1 */经过这条SQL判定,被保存的
BookStore对象通过关联属性Book.authors可以找到一些即将被脱钩的Book对象。但是,他们不同意被脱钩,抛 出如下异常Save error caused by the path: "<root>.books":
Cannot dissociate child objects
because the
dissociation action of the many-to-one property
"com.yourcommany.yourproject.model.Book.store"
is not configured as "set null" or "cascade".
There are two ways to resolve this issue:
Decorate the many-to-one property
"com.yourcommany.yourproject.model.Bookstore" by
@org.babyfish.jimmer.sql.OnDissociate whose argument
is `DissociateAction.SET_NULL` or `DissociateAction.DELETE`,
or use save command's runtime configuration to override it
4. SET_NULL
SET_NULL模式通过把关联对象的外键属性设置为null来达到脱钩目的。
SET_NULL模式要求基于外键的关联属性 (这里的Book.store) 必须可为null,否则会编译报错
脱钩模式的设置,既可以通过实体定义来设置,也可以通过保存指令来设置。
这里,我们通过实体配置来设置Book.store的脱钩模式
- Java
- Kotlin
@Entity
@KeyUniqueConstraint(noMoreUniqueConstraints = true)
public interface Book {
@OnDissociate(DissociateAction.SET_NULL)
@Nullable
@ManyToOne
BookStore store();
...省略其他代码...
}
@Entity
@KeyUniqueConstraint(noMoreUniqueConstraints = true)
interface Book {
@OnDissociate(DissociateAction.SET_NULL)
@ManyToOne
val store: BookStore?
...省略其他代码...
}
为了演示更简单的SQL生成,我们假设sqlClient的targetTransferable功能被打开,这是之前已经介绍过的内容,请参见这里
运行前文的保存代码,最终会生成三条SQL
-
保存根对象并非本文的讨论重点,默认折叠
- H2
- Mysql
- Postgres
merge into BOOK_STORE(
NAME
) key(NAME) values(
?
)
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
insert into BOOK_STORE(
NAME
) values(
? /* MANNING */
) on duplicate key update
/* fake update to return all ids */ ID = last_insert_id(ID) -
insert into BOOK_STORE(
NAME
) values(
? /* AMAZON */
) on duplicate key update
/* fake update to return all ids */ ID = last_insert_id(ID)
insert into BOOK_STORE(
NAME
) values(
?
) on conflict(
NAME
) do update set
/* fake update to return all ids */ NAME = execluded.NAME
returning ID
/* batch-0: [MANNING] */
/* batch-1: [AMAZON] */- 假设
MANNING存在,现有id为2 - 假设
AMAZON存在,插入后,数据库自动编号新分配的id为100
-
保存关联及关联对象也并非本文的讨论重点,默认折叠
- H2
- Mysql
- Postgres
merge into BOOK(
NAME, EDITION, PRICE, STORE_ID
) key(
NAME, EDITION
) values(
?, ?, ?, ?
)
/* batch-0: [Learning GraphQL, 3, 51.9, 1] */
/* batch-1: [Learning GraphQL, 4, 43.9, 1] */
/* batch-2: [Effective TypeScript, 3, 88.9, 1] */
/* batch-3: [Effective TypeScript, 4, 85.9, 1] */
/* batch-4: [Programming TypeScript, 3, 48.9, 1] */
/* batch-5: [Programming TypeScript, 4, 47.9, 1] */
/* batch-6: [GraphQL in Action, 3, 80.9, 2] */
/* batch-7: [GraphQL in Action, 4, 81.9, 2] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
insert into BOOK(
NAME, EDITION, PRICE, STORE_ID
) values(
? /* Learning GraphQL */,
? /* 3 */,
? /* 51.9 */,
? /* 1 */
) on duplicate key udpate
/* fake update to return all ids */ ID = last_insert_id(ID),
PRICE = values(PRICE),
STORE_ID = values(STORE_ID)