跳到主要内容

拥有方特有功能

基本概念

所谓拥有方,指具备mappedBy@OneToMany@OneToOne对象。以@OneToMany为例

@Entity
public interface BookStore {

@OneToMany(mappedBy = true)
List<Book> books();
}

这种关联具备一个特殊功能

  • 自动设置子对象的逆关联

  • 配置是否允许不同父对象抢夺子对象

1. 自动设置子对象的逆关联

假设有如下实体

TreeNode.java
@Entity
public interface TreeNode {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();

@Key
String name();

@Key
@ManyToOne
@Nullable
TreeNode parent();

@OneToMany(mappedBy = "parent")
List<TreeNode> childNodes();
}

TreeNode@Key属性为nameparent,除了明确抗可接受wild对象的保持模式外,被保存对象需要

  • 要么指定TreeNode.id属性

  • 要么指定TreeNode.nameTreeNode.parent属性

然而,如下代码可以正常运行

TreeNode rootNode = Immutables.createTreeNode(root -> {
root.setName("Root");
root.setParent(null);
root.addIntoChildNodes(child -> {
child.setName("Child-1");
// 对于非根对象而言,无需指定`parent`属性
});
root.addIntoChildNodes(child -> {
child.setName("Child-2");
// 对于非根对象而言,无需指定`parent`属性
});
});
sqlClient
.saveCommand(rootNode)
.setTargetTransferModeAll(TargetTransferMode.ALLOWED)
.execute();

代码中的setTargetTransferModeAll(TargetTransferMode.ALLOWED)并非这里关注的焦点,并读者先行忽略之。

这里,虽然根对象 (Root)nameparent属性都被指定了, 但是对于非根对象 (Child-1, Child-2) 而言,只有name属性被指定了,但parent属性并未被指定。

TreeNode.childNodes属性是TreeNode.parent属性互为逆向关联。

信息

对于主动端的多对一 (或一对一) 关联 (这里的TreeNode.parent) 而言, 一旦通过其从动端的一对多 (或一对一) 关联 (这里的TreeNode.childNodes) 为父对象指定了子对象集合, 那么集合中的每一个子对象的父对象引用都会被自动设置。

在本例子中,用户原本期望保存的对象树为

{
"name":"Root",
"parent":null,
"childNodes":[
{"name":"Child-1"},
{"name":"Child-2"}
]
}

假设根元素被插入后数据库为根对象分配的自动编号为100,Jimmer会自动调整这颗树

{
"id": 100,
"name":"Root",
"parent":null,
"childNodes":[
{
"name":"Child-1",
"parent": {"id": 100}
},
{
"name":"Child-2",
"parent": {"id": 100}
}
]
}

可见,一旦完成了对父对象的保存,所有子对象的TreeNode.parent属性都会被自动设置。即,在id属性未被指定的情况下

  • 从Jimmer的角度看,所有TreeNode对象的nameparent属性都被指定了,即@Key属性都被指定了

  • 从用户的角度看,除了根对象需要同时指定nameparent属性外,其他所有对象都只需要指定name属性

上诉例子会生成三条SQL

  1. @Key查询根对象是否存在

    Purpose: COMMAND(NULL_NOT_DISTINCT_REQUIRED)
    select
    tb_1_.NODE_ID,
    tb_1_.NAME,
    tb_1_.PARENT_ID
    from TREE_NODE tb_1_
    where
    tb_1_.PARENT_ID is null
    and
    tb_1_.NAME = ? /* Root */

    这里,并没有使用数据库本身的UPSERT能力,而是通过而外查询来决定后续操作应该是INSERT还是UPDATE

    这是因为数据库本身的UPSERT能力依赖于唯一性约束 (或唯一索引),这里,被保存的根对象的parent属性为null, 并非所有数据库都具备为唯一约束定义null的行为的能力。

    因此,默认情况下下,如果被保存对象的@Key属性为null,Jimmer会放弃使用数据本身的UPSERT能力, 执行额外的查询来判断后续操作应该是INSERTUPDATE,并向开发人员报告QueryReason.NULL_NOT_DISTINCT_REQUIRED

    信息

    某些数据库,例如Postgres,可以为唯一约束定义null的行为。 如何在这类数据库中解决此问题并非本文关注点,请查阅QueryReason.NULL_NOT_DISTINCT_REQUIRED的文档注释以了解更多。

  2. 假设上述查询判断被保存对象在数据库中不存在,直接插入根对象即可

    insert into TREE_NODE(NAME, PARENT_ID) 
    values(?, ?)
    /* batch-0: [Root, DbNull{type=long}] */
  3. 保存子对象 (假设保存根对象后得知其id为100)

    merge into TREE_NODE(
    NAME, PARENT_ID
    ) key(
    NAME, PARENT_ID
    ) values(?, ?)
    /* batch-0: [Child-1, 100] */
    /* batch-1: [Child-2, 100] */

2. 配置是否允许不同父对象抢夺子对象

保守的默认行为

让我们先来看一个案例

BookStore store = Immutables.createBookStore(draft -> {
draft.setName("MANNING");
draft.addIntoBooks(book -> {
book.setId(12L);
});
draft.addIntoBooks(book -> {
book.setId(1L);
});
});
sqlClient.save(store);

执行这样的代码,执行如下SQL并导致异常

  1. 保存根对象BookStore

    merge into BOOK_STORE(
    NAME
    ) key(NAME) values(?)
    /* batch-0: [MANNING] */
  2. 保存子对Book

    // highlight-next-line
    Purpose: COMMAND(TARGET_NOT_TRANSFERABLE)
    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION,
    tb_1_.STORE_ID
    from BOOK tb_1_
    where
    tb_1_.ID = any(? /* [12, 1] */)

    奇怪的是,尽管子对象的id属性被指定了 (这通常意味着Jimmer会利用数据库本身的UPSERT能力), 但Jimmer仍然尝试通过查询来判断后续操作应该是INSERT还是UPDATE,而非利用数据库本身的UPSERT能力。 更重要的是,Jimmer报告了QueryReason.TARGET_NOT_TRANSFERABLE

  3. 最终,上述代码还会导致如下异常

    Save error caused by the path: "<root>.books": 
    Can the move the child object whose type is "org.doc.j.model.Book"
    and id is "1" to another parent object because the property
    "org.doc.j.model.BookStore.books"
    does not support target transfer

Book.store是多对一关联,一个Book对象,只可能隶属于一个BookStore对象,而无法同时隶属于多个BookStore对象。

因此,通过逆向的一对多关联BookStore.books保存数据结构,就是建立从当前BookStore父对象到另外一个已经存在的Book对象之间的关联, 如果Book对象已经隶属于另外一个父对象,会导致当前父对象从其他父对象抢夺子对象。或者说,子对象在不同父对象之间发生了迁移。

如果这是开发人员预料之中的行为,那自然没有问题。但是,如果这并非开发人员预料中的行为,可能导致无意的疏忽。

默认情况下,Jimmer采用保守的策略,禁止子对象在不同父对象之间发生了迁移。

在此例中,企图让BookStore(MANNING)Book(12)Book(1)关联起来, Jimmer以QueryReason.TARGET_NOT_TRANSFERABLE为由执行额外的查询,检查是否有子对象在不同父对象之间发生了迁移。

  • Book(12)已经隶属于BookStore(MANNING),未发生迁移,没问题
  • Book(1)并不隶属于BookStore(MANNING),而隶属于BookStore(O'REILLY),发生了迁移,因此最终抛出异常。

默认行为非常保守,虽然避免了不同父对象对子对象的抢夺 (如果开发人员认为这种无意抢夺对业务是有害的), 但导致了额外的查询,并未充分发挥数据库本身UPSERT能力,性能不佳。

如果你认为更优的性能比这种保守的防御行为更重要,Jimmer提供额外配置,改变这种行为。

覆盖默认行为,不加限制

为性能优先,要取消这种限制,有两种方法

  1. 保存指令级配置,又可分为两种

    1. 精确配置,对某个关联放开限制

      BookStore store = ......;
      sqlClient
      .saveCommand(store)
      .setTargetTransferMode(
      BookStoreProps.BOOKS,
      TargetTransferMode.ALLOWED
      )
      .execute();
    2. 盲目配置,对所有关联放开限制

      BookStore store = ......;
      sqlClient
      .saveCommand(store)
      .setTargetTransferModeAll(
      TargetTransferMode.ALLOWED
      )
      .execute();

    无论精确配置,还是盲目配置,最后一个参数都是TargetTransferMode枚举,具有一下三个取值

    • ALLOWD: 允许子对象迁移,并尽可能采用数据库本身的UPSERT能力

    • NOT_ALLOWED: 不允许子对象迁移,以QueryReason.TARGET_NOT_TRANSFERABLE为由发起而外查询加以验证。 如果发生了子对象迁移,抛出异常

    • AUTO(默认):当前配置无效,参考优先级更低的配置

      • 对于精确配置而言,转而参考盲目配置

      • 对于盲目配置而言,转而参考全局配置

  2. 全局配置,又可分为两种

    1. 基于Jimmer Api的全局配置

      JSqlClient sqlCient = JSqlClient
      .newBuilder()
      .setTargetTransferable(true)
      ...省略其他配置...
      .build();
    2. 如果采用Jimmer提供的spring-boot-starter,基于Spring Boot的全局配置

      application.yml文件为例

      jimmer:
      target-transferable: true
      ...省略其他配置...

一旦通过以上任何配置手段让Jimmer认为无需对BookStore.books关联禁止子对象迁移,修改代码如下

Book.java
public interface Book {

@ManyToOne
@Nullable
// 于本文讨论内容无关,请读者先行忽略
@OnDissociate(DissociateAction.SET_NULL)
BookStore store();

...省略其他代码...
}

重新执行代码,将会生成如下SQL

  1. 保存根对象

    merge into BOOK_STORE(
    NAME
    ) key(NAME) values(?)
    /* batch-0: [MANNING] */
  2. 建立根对象和子对象之间的关联

    merge into BOOK(
    ID, STORE_ID
    ) key(ID) values(?, ?)
    /* batch-0: [12, 2] */
    /* batch-1: [1, 2] */
  3. 断开根对象和不再需要的子对象之间的关联

    update BOOK
    set
    STORE_ID = null
    where
    STORE_ID = ?
    and
    not (
    ID = any(?)
    )
    /* batch-0: [2, [12, 1]] */
信息

为了向用户展示性能优先的场景,附带例子 jimmer-examples/java/save-commandjimmer-examples/kotlin/save-command-kt 均利用全局配置允许了子对象迁移。