跳到主要内容

关联分类

基本概念

可以从两种角度对关联对象进行分类,从每个角度看都有两种关联类型,共计4种

  • 按照关联对象形状分类

    • 短关联

      只修改当前对象和其他对象之间的关联关系,不会进一步保存关联对象。

      递归保存行为被终止,不会继续深入。

    • 长关联

      不但可以修改当前对象和其他对象之间的关联关系,还会进一步保存关联对象。

      递归保存行为不会被终止,会继续深入。

  • 按照保存顺序分类

    • 前置关联

      关联对象比当前对象更早保存,其实就是基于于外键 (无论真伪) 的关联。

      例如:本教程中的Book.store

    • 后置关联

      关联对象比当前对象更晚保存,包括两种情况

      • 前置关联的逆关联。

        例如:本教程中的BookStore.books

      • 基于中间表的关联。

        例如:本教程中的Book.authorsAuthor.books

1. 按照关联对象形状分类

1.1. 短关联

所谓短关联,表示仅修改当前对象和其他对象之间的关联本身,对关联对象的修改没兴趣。

通常,UI设计会采用单选框 (关联引用) 或多选框 (关联集合)

Book Form

 

其中

  • 单选框对应多对一关联Book.store

  • 多选框对应多对多关联Book.authors

备注

实际项目中,待选数据可能很多,并不适合设计为下拉UI。这时,可以使用具备筛选条件和分页功能的对象框来代替下拉框,这是一种常见的变通方式。

由于用户只想修改当前对象和其他对象的关联,不想进一步修改关联对象,所以UI不可能出现多层关联嵌套。这就是称其为 短关联 的原因。

为save指令传递任意形状的数据结构作为参数时,指定短关联有两种方法

  • 将Id-Only对象作为关联对象

  • 在启用专用配置的前提下,将Key-Only对象作为关联对象

1.1.1. 将Id-Only对象作为关联对象

让关联对象只有id属性

Book book = Immutables.createBook(draft -> {
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("39.9"));

// 关联对象只有id属性
draft.setStoreId(2L);

draft.addIntoAuthors(author -> {
// 关联对象只有id属性
author.setId(4L);
});
draft.addIntoAuthors(author -> {
// 关联对象只有id属性
author.setId(5L);
});
});
sqlClient.save(book);
备注

这里对被保存数据结构进行硬编码仅为示范,实际项目中被保存的数据结构由前端界面提交。

当然,如果用户按照映射篇/进阶映射/视图属性/IdView一文的方法定义了authorIds属性,上述代码可以被简化,例如:

Book book = ImmutableObjects.createBook(draft -> {
draft.setAuthorIds(Arrays.asList(4L, 5L));
});

但这并不是必须的,为了让例子更具普适性,这里并不假设用户为实体类型定义了IdView属性。后续所有文档都如此,不再提醒。

假设当前数据库为H2,生成的SQL

  1. 保存聚合根。

    merge into BOOK(
    NAME, EDITION, PRICE, STORE_ID
    ) key(
    NAME, EDITION
    ) values(?, ?, ?, ?)
    /* batch-0: [SQL in Action, 1, 39.9, 2] */

    由于Book.store是直接基于外键 (STORE_ID) 的多对一关系,当前对象和BookStore(2)对象的关联将会因这条SQL的执行而被自动创建。

  2. 如果和当前对象 (新插入的数据为Book(100)) 关联的Author对象不仅仅只有Author(4)Author(5),切断和其他对象啊的关联。

    delete from BOOK_AUTHOR_MAPPING
    where
    BOOK_ID = ? /* 100 */
    and
    not (
    AUTHOR_ID = any(? /* [4, 5] */)
    )

    这个步骤叫做脱勾,后续文档会给予介绍,这里请读者先行忽略

  3. 建立对象 (新插入的数据为Book(100))Author(4)Author(5)两个对象之间的关联

    merge into BOOK_AUTHOR_MAPPING tb_1_ 
    using(values(?, ?)) tb_2_(
    BOOK_ID, AUTHOR_ID
    )
    on
    tb_1_.BOOK_ID = tb_2_.BOOK_ID
    and
    tb_1_.AUTHOR_ID = tb_2_.AUTHOR_ID
    when not matched then insert(BOOK_ID, AUTHOR_ID)
    values(tb_2_.BOOK_ID, tb_2_.AUTHOR_ID)
    /* batch-0: [100, 4] */
    /* batch-1: [100, 5] */
信息

通过此例,不难发现,短关联只会创建或销毁当前对象和其他对象之间的关联关系,不会进一步保存关联对象。

短关联总是假设被引用的对象是存在的,如果所引用的对象 (这个例子中的BookStore(2), Author(4)Author(5)) 不存在,会导致异常!

1.1.2. 在启用专用配置的前提下,将Key-Only对象作为关联对象

下面代码,假设

  • BookStore类型的key属性是name

  • Author类型的key属性是firstNamelastName

    实际业务中,这个唯一性约束未必合理,这里为简化例子,姑且这样假设。

Book book = Immutables.createBook(draft -> {
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("39.9"));
draft.applyStore(store -> {
// 关联对象只有key属性,即`BookStore.name`
store.setName("MANNING");
});
draft.addIntoAuthors(author -> {
// 关联对象只有key属性,即`Author.firstName`和`Author.lastName`
author.setFirstName("Boris").setLastName("Cherny");
});
draft.addIntoAuthors(author -> {
// 关联对象只有key属性,即`Author.firstName`和`Author.lastName`
author.setFirstName("Samer").setLastName("Buna");
});
});
sqlClient
.saveCommand(book)
.setKeyOnlyAsReference(BookProps.STORE)
.setKeyOnlyAsReference(BookProps.AUTHORS)
.execute();
信息

默认情况下key-only关联对象被视为长关联

然而,开发人员可以通过调用setKeyOnlyAsReference方法将key-only关联对象视为短关联。

  • 这里,两次调用setKeyOnlyAsReference方法,明确地配置关联Book.storeBook.authors

    事实上,你也可以调用一次setKeyOnlyAsReferenceAll方法,盲目地配置所有关联关系。

  • 和Kotlin相比,Java API对保存指令的高级配置的便捷性稍低。

    先调用saveCommand方法创建一个保存指令但不立即执行,完成高级配置后,再调用execute方法真正执行。

1.2. 长关联

所谓长关联,表示除了要修改当前对象和其他对象之间的关联本身外,还要进一步修改关联对象。

通常,订单和订单明细是这类场景的最佳示范,UI设计会采用内嵌表格,例如

商品数量单价明细价删除
268536
238238
总额:774
 

由于用户不但要修改当前对象和其他对象的关联,还要进一步修改关联对象,而关联对象可以包含更深的关联,所以,理论上讲,UI可出多层关联嵌套。这就是称其为 长关联 的原因。

备注

虽然设计人员为了保证UI的简洁性会刻意避免在内嵌表格中嵌套更深的内嵌表格,但是实际项目中仍然存在需要在UI上维护多层嵌套关联的场景,比如:

  • 表单本身是一颗树结构,编辑好了后,作为一个整体保存。

  • 可视化UI设计,因为UI组件本身就是树形结构,用户进行一系列可视化拖拉拽的设计后,把UI组件树作为一个整体保存。

Jimmer可以直接保存任意形状的长关联数据结构,如果把深度未知的长关联数据结构称为复杂表单,保存指令就是为复杂表单而设计。

例子如下

Order order = Immutables.createOrder(draft -> {
draft.setCustomerId(1L);
draft.setProvince("四川");
draft.setCity("成都");
draft.setAddress("龙泉驿区洪玉路与十洪路交叉口");
draft.addIntoItems(item -> {
item.setProductId(8L);
item.setQuantity(2);
});
draft.addIntoItems(item -> {
item.setProductId(9L);
item.setQuantity(1);
});
});
sqlClient.insert(order);

在这个例子中,我们可以看到很多短关联,例如Order.customer, OrderItem.product,但是,们并非这里应该关注的重点。

在这里,我们应该关注关联Order.items,很明显,它是一个长关联。

此操作会生成两条SQL

  1. 插入根对象Order

    insert into order_(
    PROVINCE, CITY, ADDRESS, CUSTOMER_ID
    ) values(?, ?, ?, ?)
    /*batch-0: [四川, 成都, 龙泉驿区洪玉路与十洪路交叉口, 1] */
  2. 插入所有子对象OrderItem

    insert into ORDER_ITEM(
    ORDER_ID,
    PRODUCT_ID,
    QUANTITY
    ) values(?, ?, ?)
    /* batch-0: [100, 8, 2] */
    /* batch-1: [100, 9, 1] */
信息

由此可见,长关联不仅能修改当前对象和其他对象的关联关系,还是会导致关联对象被保存。

如果关联对象也具备长关联,将会递归保存,直到没有更多关联属性或遇到短关联为止。

2. 按照保存顺序分类

2.1. 前置关联

前置关联就是基于外键 (无论真伪) 的关联,其工作模式为,先保存关联对象,再保存根对象。

Book book = Immutables.createBook(draft -> {
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("49.9"));
draft.applyStore(store -> {
store.setName("TURING");
store.setWebsite("https://www.turing.com");
});
});
sqlClient.save(book);

以H2为例,生成两条SQL

  1. 先保存关联对象BookStore

    merge into BOOK_STORE(
    NAME, WEBSITE
    ) key(NAME) values(?, ?)
    /* batch-0: [TURING, https://www.turing.com] */
  2. 后保存当前对象Book

    merge into BOOK(
    NAME, EDITION, PRICE, STORE_ID
    ) key(
    NAME, EDITION
    ) values(?, ?, ?, ?)
    /* batch-0: [SQL in Action, 1, 49.9, 100] */
警告

在工作交流中,面对前置关联时,建议用"当前对象/关联对象"这样的方式来表达,而不是"父对象/子对象"这种表达方式。

因为,对于前置关联而言,ORM层面的父子关系和数据库建模层面的父子关系完全相反,非常容易引起混淆和误会。

2.2. 后置关联

其他关联,例如

  • 前置关联的逆关联,(本教程中的BookStore.books)
  • 基于中间表的关联,(本教程中的Book.authorsAuthor.books)

都可以归为后置关联,是一种更常见的场景。

后置关联的工作模式更容易理解,先保存当前对象,再保存关联对象。

BookStore store = Immutables.createBookStore(draft -> {
draft.setName("TURING");
draft.setWebsite("https://www.turing.com");
draft.addIntoBooks(book -> {
book.setName("SQL in Action");
book.setEdition(1);
book.setPrice(new BigDecimal("49.9"));
});
draft.addIntoBooks(book -> {
book.setName("RUST programming");
book.setEdition(2);
book.setPrice(new BigDecimal("39.9"));
});
});
sqlClient
.saveCommand(store)
// 请读者先行忽略此配置
.setTargetTransferModeAll(TargetTransferMode.ALLOWED)
.execute();

以H2为例,生成三条SQL

  1. 先保存当前对象BookStore

    merge into BOOK_STORE(
    NAME, WEBSITE
    ) key(
    NAME
    ) values(?, ?)
    /* batch-0: [TURING, https://www.turing.com] */
  2. 后保存关联对象Book

    merge into BOOK(
    NAME, EDITION, PRICE, STORE_ID
    ) key(
    NAME, EDITION
    ) values(?, ?, ?, ?)
    /* batch-0: [SQL in Action, 1, 49.9, 100] */
    /* batch-1: [RUST programming, 2, 39.9, 100] */
  3. 第三条SQL和这里讨论的话题无关,省略

信息

后置关联的功能比前置关联丰富,本教程讲重点讨论后置关联