跳到主要内容

保存关联

关联类型

站在用户角度,被保存数据结构中存在两种关联

短关联

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

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

Book Form

 

其中

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

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

备注

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

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

信息

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

  • 要么让关联对象只有id属性

  • 要么让关联对象只有key属性

两种方法示范如下

  • 让关联对象只有id属性

    Book book = Objects.createBook(draft -> {
    draft.setName("SQL in Action");
    draft.setEdition(1);
    draft.setPrice(new BigDecimal("39.9"));
    draft.applyStore(store -> {
    // 关联对象只有id属性
    store.setId(2L);
    });
    draft.addIntoAuthors(author -> {
    // 关联对象只有id属性
    author.setId(4L);
    });
    draft.addIntoAuthors(author -> {
    // 关联对象只有id属性
    author.setId(5L);
    });
    });
    sqlClient.save(book);
    备注

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

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

    draft.setAuthorIds(Arrays.asList(4L, 5L));

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

  • 让关联对象只有key属性

    备注

    下面代码,假设

    • BookStore类型的key属性是name

    • Author类型的key属性是firstNamelastName

      实际项目中,这个唯一性约束并不合理,这里为简化例子,姑且这样假设,请读者对此细节别太较真

    Book book = Objects.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.save(book);

长关联

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

通常,UI设计会采用内嵌表格,例如

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

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

备注

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

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

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

信息

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

  • 如果已经为关联对象指定了id属性,继续为对象指定任何非id属性 (包括key属性)

  • 否则,必须为关联对象指定了所有key属性和其他属性。

无论是以上哪种方式,至少为关联对象指定一个既非id也非key的属性。

例子如下

Order order = Objects.createOrder(draft -> {
draft.applyCustomer(customer -> customer.setId(1L));
draft.setProvince("四川");
draft.setCity("成都");
draft.setAddress("龙泉驿区洪玉路与十洪路交叉口");
draft.addIntoItems(item -> {
item.applyProduct(product -> product.setId(1L));
// 既非id也非key的属性
item.setQuantity(2);
});
draft.addIntoItems(item -> {
item.applyProduct(product -> product.setId(10L));
// 既非id也非key的属性
item.setQuantity(1);
});
});
sqlClient.save(order);
备注

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

一对多关联的特殊性

之前提到

信息

如果实体类型声明了Key属性 (也意味着id除了充当唯一标识符外无任何业务意义),那么

  • 对于被保存的聚合根对象而言,建议为其指定id属性或key属性。否则会被理解成强行插入。

  • 对于本文正在讨论的待保存的关联对象而言,要么指定id属性,要么指定所有key属性。否则会异常。

但是,有一种情况例外,一对多关系。

让我来看一个例子

实体定义

TreeNode.java
@Entity
public interface TreeNode {

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

@Key
String name();

@Key
@Nullable
@ManyToOne
TreeNode parent();

...省略其他属性...
}

使用保存指令

TreeNode treeNode = Objects.createTreeNode(food -> {
food
.setParent(null)
.setName("Food")
.addIntoChildNodes(drink -> {
drink
.setName("Drink")
.addIntoChildNodes(cococola -> {
cococola.setName("Cococola");
})
.addIntoChildNodes(fanta -> {
fanta.setName("Fanta");
});
;
})
.addIntoAuthors(bread -> {
bread
.setName("Bread")
.addIntoChildNodes(daguette -> {
daguette.setName("Baguette");
})
.addIntoChildNodes(ciabatta -> {
ciabatta.setName("Ciabatta");
})
});
;
});
sqlClient.save(treeNode);

TreeNode类型的key属性为nameparent

  • 对根节点而言,如果没有指定id属性,建议指定nameparent属性,否则会被理解为强行插入。

  • 对子节点而言,如果没有指定id属性,则必须指定nameparent属性,否则异常。

然而,在这这个例子中,除了根节点对象外,其他所有对象的parent属性都未被指定。这段代码看起来似乎无法正常执行。

事实上,这段代码可以正确运行,因为save指令为一对多关联做了一个特殊的规定:

提示

如果被保存的数据结构中,父对象通过一对多关联 (本例中为TreeNode.childNodes) 持有了某些子对象, 那么这些子对象的逆向多对一关联 (本例中为TreeNode.parent) 将会被保存指令视为已被设置。

所以,上例中,保存指令会认为所有子节点的parent属性都已经被设置了。即,所有子对象都同时具备nameparent属性,保存指令可顺利运行。

脱勾操作

对于关联对象而言,插入或更新操作和之前介绍过的保存聚合根对象操作并无较大差异。用户只需要记住关联对象不受保存模式控制 (也可认为关联对象的保存模式只有UPSERT) 即可。

然而,关联对象具有一个非常特殊的操作:脱勾。

脱勾操作时一个非常重要的概念,限于篇幅,我们会在下一篇文档中对其详细介绍。