短关联id检查
基本概念
只针对短关联
关联id检查是一个只针对
的功能,对无意义。通过之前的介绍,我们知道保存指令可以保存任意形状的数据结构,任何对象都可以进一步持有关联对象。
如果某个关联对象的id被指定了,但其所代表的对象在数据库中不存在是,Jimmer如何应对呢?
首先,对于
而言,Jimmer会先创建不存在的关联对象,然后建立当前对象和新关联对象之间的关联。例如:- Java
- Kotlin
sqlClient.update(
Immutables.createBook(draft -> {
draft.setId(3L);
draft.addIntoAuthors(author -> author.setId(1L)); // ❶
draft.addIntoAuthors(author -> author.setId(2L)); // ❷
draft.addIntoAuthors(author -> { // ❸
author.setId(1000L);
author.setFirstName("Svetlana");
author.setLastName("Isakova");
author.setGender(Gender.FEMALE);
});
})
);
sqlClient.update(
Book {
id = 3L
authors().addBy { id = 1L } // ❶
authors().addBy { id = 2L } // ❷
authors().addBy { // ❸
id = 1000L
firstName = "Svetlana"
lastName = "Isakova"
gender = Gender.FEMALE
}
}
)
这个例子混合了长关联和短关联。
-
❶ ❷ 它们是
,一旦指定了非法id,就会导致错误。 -
❸ 这是
,即使被指定了非法id,Jimmer也会自动创建该关联对象。
生成的SQL如下
// 判断关对象是否存在
select
tb_1_.ID,
tb_1_.FIRST_NAME,
tb_1_.LAST_NAME
from AUTHOR tb_1_
where
tb_1_.ID = ? /* 1000 */
// 关联对象不存在,创建之
// highlight-next-line
insert into AUTHOR(ID, FIRST_NAME, LAST_NAME, GENDER)
values
(? /* 1000 */, ? /* Svetlana */, ? /* Isakova */, ? /* F */)
// 查询当前`Book`和`Author`之间的的关系
select
AUTHOR_ID
from BOOK_AUTHOR_MAPPING
where
BOOK_ID = ? /* 3 */
// 连接当前`Book`和刚被新建的`Author`
insert into BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
values
(? /* 3 */, ? /* 1000 */)
因此,关联id检查
是一个仅对
概念定义:目标外键
要讨论短关联的id检查问题,我们先为关联属性定义定一个概念:目标外键。
-
如果关联基于中间表,则中间表中指向目标实体表的外键就是目标外键。
比如:
-
Book.authors
的目标外键是BOOK_AUTHOR_MAPPING
表的AUTHOR_ID
字段。 -
Author.books
的目标外键是BOOK_AUTHOR_MAPPING
表的BOOK_ID
字段。
-
-
如果关联基于外键,无论外键真假 (请参见真假外键),关联本身的外键就是目标外键。
比如:
Book.store
的目标外键是BOOK
表的STORE_ID
字段。 -
如果以上两个情况都不是,则认为关联没有目标外键。
没有目标外键的关联属性,就是被指定了
mappedBy
的一对一或一对多属性。即,被@OneToOne(mappedBy = "...")
或@OneToMany(mappedBy="...")
修饰的属性。比如:
BookStore.books
没有目标 外键。
总结
关联 | 目标外键的列名 | 目标外键所在的表 |
---|---|---|
Book.authors | AUTHOR_ID | BOOK_AUTHOR_MAPPING |
Author.books | BOOK_ID | BOOK_AUTHOR_MAPPING |
Book.store | STORE_ID | BOOK |
BookStore .books | NA | NA |
检查机制
用户可以配置是否检查短关联对象的id。
这里,暂不讨论如何配置,让我们先看是否开启此配置对Jimmer的行为有何影响。
-
没有目标外键的属性
以
BookStore.books
为例,保存短关联的代码为- Java
- Kotlin
sqlClient.update(
Immutables.createBookStore(draft -> {
draft.setId(2L);
draft.addIntoBooks(book -> book.setId(8L));
draft.addIntoBooks(book -> book.setId(9L));
draft.addIntoBooks(book -> book.setId(1000L));
draft.addIntoBooks(book -> book.setId(1001L));
})
);sqlClient.update(
BookStore {
id = 2L
books().addBy { id = 8L }
books().addBy { id = 9L }
books().addBy { id = 1000L }
books().addBy { id = 1001L }
}
);-
不检查
没有目标外键的属性,id非法的所有子对象将会被自动忽略。比如
update book set store_id = 2 where id in(1, 2, 1000, 10001)
假如1000,10001是数据库中不存在的id,那么这条update语句只会影响两条存在的数据,不存在的两条数据会被自然地忽略。
-
要检查
Jimmer会执行如下查询检查所有短关联的id
select
tb_1_.ID
from BOOK tb_1_
where
tb_1_.ID in (
? /* 1 */, ? /* 2 */, ? /* 1000 */, ? /* 1001 */
)假如,数据库中不存在id为1000和1001的书籍,会得到如下异常
Save error caused by the path: "<root>.books": Illegal ids: [1000, 1001]
-
有目标外键的属性
以
Book.store
为例,保存短关联的代码为- Java
- Kotlin
sqlClient.update(
Immutables.createBook(draft -> {
draft.setId(10L);
draft.applyStore(store -> store.setId(321L));
})
);sqlClient.update(
Book {
id = 10L
store { id = 321L }
}
);假如数据库中并没有id为321的BookStore
-
不检查
-
如果外键是假的,在数据库中并没有真正的外键约束,那么Jimmer就会纵容
BOOK.STORE_ID
被修改为非法的值。 -
如果外键是真的,在数据库中有真正的外键约束,那么最终底层数据库报错。
-
-
要检查
无论外键真假,Jimmer都会执行如下查询检查短关联的id
select
tb_1_.ID
from BOOK_STORE tb_1_
where
tb_1_.ID in (
? /* 321 */
)一旦查询不到任何数据,得到如下异常
Save error caused by the path: "<root>.store": Illegal ids: [321]
总结
真目标外建 | 假目标外建 | 无目标外建 | |
---|---|---|---|
不检查 | 由数据库报错 | 保存错误数据 | 忽略非法操作 |
检查 | 由Jimmer报错 | 由Jimmer报错 | 由Jimmer报错 |
可见,对于目标外键为真的属性而言,无论是否是否启用Jimmer的短关联id检查,都会得到异常。
-
不验证,由数据库报错。
-
好处:少执行一条SQL查询
-
坏处:异常信息和异常类型难控制
-
-
验证,由Jimmer报错。
-
好处:异常信息和异常类型明确
-
坏处:多执行一条SQL查询
-
只要项目不是对修改业务性能要求到吹毛求疵的那种,就建议此检查机制对所有属性全开,以得到理想的异常信息 (稍后我们会介绍如何配置)。
配置
用户可以配置关联属性是否检察关联id。分为全局配置和指令级配置。
全局配置
全局配 置提供了三个等级
NONE
FAKE
ALL
功能如下
真目标外建 | 假目标外建 | 无目标外建 | |
---|---|---|---|
NONE | 不检查 | 不检查 | 不检查 |
FAKE | 不检查 | 检查 | 检查 |
ALL | 检查 | 检查 | 检查 |
全局配置有两种实现方法
-
通过SpringBoot Starter配置
修改
application.yml
(或application.properties
),添加如下配置jimmer:
id-only-target-checking-level: ALL -
通过底层API配置
- Java
- Kotlin
JSqlClient sqlClient = JSqlClient
.newBuilder()
.setIdOnlyTargetCheckingLevel(IdOnlyTargetCheckingLevel.ALL)
...省略其他配置...
.build();val sqlClient = newKSqlClient {
setIdOnlyTargetCheckingLevel(IdOnlyTargetCheckingLevel.ALL)
...省略其他配置...
}
指令级配置
指令级别配置可以覆盖全局配置,但仅仅影响当前保存指令。
指令级配置有三个功能点,如下
-
明确指定属性需要检查
- Java
- Kotlin
Book book = ...略...
sqlClient
.getEntities()
.saveCommand(book)
.setAutoIdOnlyTargetChecking(BookProps.STORE)
.setAutoIdOnlyTargetChecking(BookProps.AUTHORS)
.execute();val book = ...略...
sqlClient.save(book) {
setAutoIdOnlyTargetChecking(Book::store)
setAutoIdOnlyTargetChecking(Book::authors)
}信息如果全局配置已经打开了检查机制,就不需要如此调整保存指令了。
-
指定所有属性需要检查
- Java
- Kotlin
Book book = ...略...
sqlClient
.getEntities()
.saveCommand(book)
.setAutoIdOnlyTargetCheckingAll()
.execute();val book = ...略...
sqlClient.save(book) {
setAutoIdOnlyTargetCheckingAll()
}信息如果全局配置已经打开了检查机制,就不需要如此调整保存指令了。
-
负配置,明确指定属性不需要检查
- Java
- Kotlin
Book book = ...略...
sqlClient
.getEntities()
.saveCommand(book)
.setAutoIdOnlyTargetCheckingAll()
.setAutoIdOnlyTargetChecking(BookProps.STORE, false)
.execute();val book = ...略...
sqlClient.save(book) {
setAutoIdOnlyTargetCheckingAll()
setAutoIdOnlyTargetChecking(Book::authors, false)
}