脱钩操作
在采用关联模式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?
...省略其他代码...
}