数据建模: 组合
本指南将覆盖实体间的组合关系。将会演示关联(Association) 和组合(Composition)之间的不同。会使用示例探究两种不同的组合:一对多组合和一对一组合。
将要构建的内容
本指南对 CUBA 宠物诊所 进行了增强,以演示实体间组合关系的各种用例。特别是会覆盖以下用例:
-
Owner
实体与Address
建立关联 -
Pet
包含多个HealthRecord
实例,每个HealthRecord
包含多个HealthRecordAttachment
。 -
The
Employee
entity has a detail entity for storing the employee record:EmployeeRecord
which will be entered during the creation process of theEmployee
-
Employee
实体有一个用于存储雇员详细信息实体:EmployeeRecord
,该实体信息是在雇员的创建过程中录入的。
开发环境要求
您的开发环境需要满足以下条件:
-
文件编辑器或者IDE (推荐使用 IntelliJ IDEA )
-
独立运行版或者IDEA插件版的 CUBA Studio (可选)
-
CUBA CLI (可选)
下载 并解压本指南的源码,或者使用 git 克隆下来:
示例: CUBA 宠物诊所
这个示例是以 CUBA 宠物诊所项目为基础,而这个项目的基础是众所周知的 Spring 宠物诊所项目。CUBA 宠物诊所应用程序涉及到了宠物诊所的领域模型及与管理一家宠物诊所相关的业务流程。
这个应用程序的领域模型如下:

主要的实体是 Pet 和 Visit。 Pet 到诊所就诊,就诊时(Vist) 会有一名兽医(Vet)负责照顾它。每个宠物都有主人,一个主人可以有多个宠物。一次就诊(Vist)即是一个宠物在主人的帮助下到诊所诊治的活动。

组合 vs. 关联
CUBA 平台支持两种类型的实体间关系: 关联(ASSOCIATION)
和 组合(COMPOSITION)
。 关联是对象间可以独立存在的关系。而组合,换句话说就是用于 “主-从”关系,这种关系中明细实实体不能脱离主实体存在。
在宠物诊所示例中,Owner
和 Addresses
之间的关系可以看作是组合:一个不属于任何 Owner
的 Addresses
没有任何意义 (在这个应用程序范围内)
典型情况下,组合关系的实体总是一起编辑,因为这样很自然、合理。用户打开 Owner
编辑界面,可以看到 addresses
列表,对 Owner
信息和 addresses
列表进行编辑,最后将所有更改在一个事务中一起提交到数据库, 并且只能在用户确定保存主实体(Owner
) 时才保存。
一对多组合
在本指南中会演示组合关系的不同用法。组合可以是 ONE-TO-MANY
,也可以是 ONE-TO-ONE
。在应用程序中甚至可以使用多级嵌套组合。
随着 CUBA 7 的发布,CUBA 应用程序不再限制嵌套层数。但是,如果嵌套层数太多,复杂的界面操作常常会使用户迷惑。因此, 建议不要使用太多层次的嵌套组合。 |
一对多: 一层嵌套
使用 Owner
和 the Address
实体作为一对多组合的示例:

-
Address.java -
Address
实体必须关联一个Owner
。在Studio提供的实体设计器中,对 owner
实体进行以下设置: Attribute type -ASSOCIATION
, Cardinality -MANY_TO_ONE
, Mandatory -on
。 -
Owner.java -
Owner
实体包含了一个一对多关系的addresses
集合属性。对应的字段使用了@Composition
注解来实现组合,使用@OnDelete
实现级联软删除。在 Studio 提供的实体设计器中,对 addresses
属性进行以下设置:Attribute type -COMPOSITION
, Cardinality -ONE_TO_MANY
, On delete -CASCADE
。 -
views.xml -
Owner
实体的owner-with-pets-and-addresses
编辑界面包含addresses
集合属性。Addresses
本身使用了_local
视图,这是因为Addresses
的owner
属性只在Addresses
创建时设置并且后续不会更改,所以就不需要加载它。 -
owner-edit.xml -
Owner
编辑界面的XML描述中为Owner
实例定义了一个数据容器,同时也包含了一个嵌套的addresses
集合容器。 XML描述中也包含一个用于管理Address
实例的表格。 -
address-edit.xml -
Address
实体的标准编辑界面。
最终,Owner
实例的编辑过程如下:
Owner
编辑界面显示了一个 addresses
列表。
修改过的 Address 实例并没有保存到数据库, 只是提交到了 Owner 编辑界面的 DataContext
。 用户可以创建新的实例,也可以删除已有实例。所有的更改都会提交到 DataContext
。
在用户点击 Owner 编辑界面上的 OK 按钮时,修改过的 Owner
实例和 Address
实例会被提交给 dataManager.commit()
方法,并在同一个事务中保存到数据库。
一对多:两层嵌套
组合也可以有多级。下面的示例展示一个两级组合的示例。在宠物诊应用中有存储宠物健康记录的需求。Pet
→ HealthRecord
关系是另外一个组合, HealthRecord
也一个组合属性用于存储健康记录的附件信息。

-
Pet.java -
Pet
类的healthRecord
属性被标记为@Composition
和@OnDelete
。 -
HealthRecord.java -
HealthRecord
类的attachments
属性被标记为@Composition
和@OnDelete
,与Pet
类的healthRecords
属性类似。 -
views.xml -
HealthRecord
类的health-record-with-attachments
视图包含了attachments
集合属性。 这个视图被用于Pet
实体的pet-with-owner-and-type-and-health-records
视图, 这个视图被作为编辑界面的根视图。 -
pet-edit.xml -
Pet
编辑界面的 XML描述定义了一个Pet
实例的数据容器,和它的healthRecords
属性的集合数据容器。 界面上也一个用于管理HealthRecord
实例的表格。这个界面使用pet-with-owner-and-type-and-health-records
视图作为编辑界面的根视图。CUBA 7+ 之前需要在顶级实体的编辑界面中为第二级嵌套的组合定义数据源,现在已经不需要了。
-
health-record-edit.xml -
HealthRecord
编辑界面的 XML 描述中为HealthRecord
实例定义了一个数据容器,也为attachments
定义了一人嵌套的集合数据容器。 有一个表格用于管理HealthRecordAttachment
实例。
最终, HealthRecordAttachment
实例和 HealthRecord
实例的更新会与 Pet
实例在同一个事务中保存到数据库。
多级嵌套
如上所述,CUBA7+ 不再限制组合关系的嵌套层数,这意味着可以使用三级(或更多)级的嵌套。示例中的两级嵌套可以继续扩展: Owner
→ Pet
→ Health Record
→ Health Record Attachment
,事实中示例程序中已经这样做了。 也可以在一个组合链中联合使用一对多和一对一组合。
要谨慎使用多级组合,因为它会为用户带来一些困惑,用户常常会分辨不出当前在嵌套层次的哪一级。
一对一组合
除了上面介绍的 一对多
组合,CUBA 平台中也允许定义 一对一
组合。 在一个实体仅有一个对应的子实体时这种类型的组合就很有用。宠物诊所中的示例场景:
宠物谁的的雇员需要管理。因此定义了一个 Employee
实体。每个 Employee
实例有一个关联的 EmployeeRecord
实体,用于存储这个雇员工作相关的信息。

-
Employee.java -
Employee
实体与EmployeeRecord
包含一个可选的一对一组合关系。 -
EmployeeRecord.java -
EmployeeRecord
实体。
在 UI 层面,有两种可能的方式处理一对一组合。通常,一对一组合在主实体的表单中显示为一个单一入口。通过这个入口打开一个独立的界面来编辑子实体的详细信息。但是也可以将主、子实体的信息显示在一个界面,并在一个界面中进行编辑。下面会展示这两种方式。
使用详细界面的的一对一关系
-
employee-edit.xml - 雇员实体编辑界面描述。为了加载嵌套的实例,根数据容器使用了
Employee
实体的employee-with-employee-record-view
视图。
在 employee-edit.xml
中 employeeRecord
属性被定义为 pickerField
组件,这个组件包含了 OpenAction
操作(使用特定类型:picker_open_composition
)和 ClearAction
操作:
<pickerField id="employeeRecordField" property="employeeRecord">
<actions>
<action id="open" type="picker_open_composition"/>
<action id="clear" type="picker_clear"/>
</actions>
</pickerField>
最终,雇员编辑界面的操作流程如下:

在点击了 pickerField
组件的"open"按钮后, 创建了一个 EmployeeRecord
实例,同时打开了相应的编辑界面。点击了 EmployeeRecord
编辑界面的 OK 按钮的一,EmployeeRecord
实例并没有马上保存到数据数据库,而是提交到了雇员编辑界面的 employeeRecordDc
数据容器。
选择器控件上显示了 EmployeeRecord
实体的实例名:

当用户点击雇员编辑界面上的 OK 按钮,修改过的 Employee
实例和 EmployeeRecord
实例一起被提交给 DataManager.commit()
方法,然后在同一个事务中保存到数据库。
如果用户点击选择器控件上的清除按钮, EmployeeRecord
实例会被删除,同时对此实例的引用会在雇员编辑界面提交后被清除。
在一个编辑界面中的一对一组合
在同一个编辑界面中编辑一对一组合比较方便。下面的示例演示如何在 Employee
编辑界面中编辑 EmployeeRecord
。
The employee-single-editor-edit.xml 界面描述中包含了 employeeDc
和嵌套的 employeeRecordDc
数据容器:
<data>
<instance id="employeeDc"
class="com.haulmont.sample.petclinic.entity.employee.Employee"
view="employee-with-employee-record-view">
<loader/>
<instance id="employeeRecordDc" property="employeeRecord"/>
</instance>
</data>
两个实体中的字段可以定义在一个表单里,也可以定义在多个表单里:
<form id="form" dataContainer="employeeDc">
<textField id="firstNameField" property="firstName"/>
<textField id="lastNameField" property="lastName"/>
<dateField id="birthdateField" property="birthdate"/>
</form>
<form id="employeeRecordForm" dataContainer="employeeRecordDc">
<textField id="personellNumberField" property="personellNumber" datatype="int"/>
<textField id="amountSickDaysFild" property="amountSickDays" datatype="int"/>
</form>
在 EmployeeSingleEditorEdit.java 界面控制器中,会创建一个新的 EmployeeRecord
实例,并且与新建的 Employee
实例关联起来:
@Inject
protected DataContext dataContext;
@Subscribe
protected void onInitEntity(InitEntityEvent<Employee> event) { (1)
Employee employee = event.getEntity();
EmployeeRecord employeeRecord = createEmployeeRecord();
employee.setEmployeeRecord(employeeRecord);
}
private EmployeeRecord createEmployeeRecord() {
return dataContext.merge(metadata.create(EmployeeRecord.class)); (2)
}
1 | 对 EmployeeRecord 的初始化操作可以在 InitEntityEvent 事件中执行 |
2 | 创建了一个 EmployeeRecord 实例并且被合并到当前 dataContext |
现在,可以创建两个关联的实体并且在同一个界面中对其进行编辑。

总结
本指南描述了数据建模中的组合关系。组合对具有主从关系的两个实体很有价值,组合关系表示子实体不能脱离主实体独立存在。
组合结构存在两种模式: 1:1和1:N。更进一步, 组合可以嵌套多级。对应的UI界面会以特殊的方式来处理组合关系。组合关系中的子实体总是与主实体一起保存。