跳到主要内容

短关联id检查

基本概念

只针对短关联

关联id检查是一个只针对

的功能,对无意义。

通过之前的介绍,我们知道保存指令可以保存任意形状的数据结构,任何对象都可以进一步持有关联对象。

如果某个关联对象的id被指定了,但其所代表的对象在数据库中不存在是,Jimmer如何应对呢?

首先,对于

而言,Jimmer会先创建不存在的关联对象,然后建立当前对象和新关联对象之间的关联。例如:

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);
});
})
);

这个例子混合了长关联和短关联。

  • ❶ ❷ 它们是

    ,一旦指定了非法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.authorsAUTHOR_IDBOOK_AUTHOR_MAPPING
Author.booksBOOK_IDBOOK_AUTHOR_MAPPING
Book.storeSTORE_IDBOOK
BookStore.booksNANA

检查机制

用户可以配置是否检查短关联对象的id。

这里,暂不讨论如何配置,让我们先看是否开启此配置对Jimmer的行为有何影响。

  • 没有目标外键的属性

    BookStore.books为例,保存短关联的代码为

    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));
    })
    );
    • 不检查

      没有目标外键的属性,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为例,保存短关联的代码为

    sqlClient.update(
    Immutables.createBook(draft -> {
    draft.setId(10L);
    draft.applyStore(store -> store.setId(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配置

    JSqlClient sqlClient = JSqlClient
    .newBuilder()
    .setIdOnlyTargetCheckingLevel(IdOnlyTargetCheckingLevel.ALL)
    ...省略其他配置...
    .build();

指令级配置

指令级别配置可以覆盖全局配置,但仅仅影响当前保存指令。

指令级配置有三个功能点,如下

  • 明确指定属性需要检查

    Book book = ......
    sqlClient
    .getEntities()
    .saveCommand(book)
    .setAutoIdOnlyTargetChecking(BookProps.STORE)
    .setAutoIdOnlyTargetChecking(BookProps.AUTHORS)
    .execute();
    信息

    如果全局配置已经打开了检查机制,就不需要如此调整保存指令了。

  • 指定所有属性需要检查

    Book book = ......
    sqlClient
    .getEntities()
    .saveCommand(book)
    .setAutoIdOnlyTargetCheckingAll()
    .execute();
    信息

    如果全局配置已经打开了检查机制,就不需要如此调整保存指令了。

  • 负配置,明确指定属性不需要检查

    Book book = ......
    sqlClient
    .getEntities()
    .saveCommand(book)
    .setAutoIdOnlyTargetCheckingAll()
    .setAutoIdOnlyTargetChecking(BookProps.STORE, false)
    .execute();