数据建模: 组合

本指南将覆盖实体间的组合关系。将会演示关联(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 the Employee

  • Employee 实体有一个用于存储雇员详细信息实体:EmployeeRecord ,该实体信息是在雇员的创建过程中录入的。

开发环境要求

你的开发环境需要满足以下条件:

下载 并解压本指南的源码,或者使用 git 克隆下来:

示例: CUBA 宠物诊所

这个示例是以 CUBA 宠物诊所项目为基础,而这个项目的基础是众所周知的 Spring 宠物诊所项目。CUBA 宠物诊所应用程序涉及到了宠物诊所的领域模型及与管理一家宠物诊所相关的业务流程。

这个应用程序的领域模型看起来像这样:

领域模型

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

组合 vs. 关联

CUBA 平台支持两种类型的实体间关系: 关联(ASSOCIATION)组合(COMPOSITION) 。 关联是对象间可以独立存在的关系。而组合,换句话说就是用于 “主-从”关系,这种关系中明细实实体不能脱离主实体存在。

在宠物诊所示例中,OwnerAddresses 之间的关系可以看作是组合:一个不属于任何 OwnerAddresses 没有任何意义 (在这个应用程序范围内)

典型情况下,组合关系的实体总是一起编辑,因为这样很自然、合理。用户打开 Owner 编辑界面,可以看到 addresses 列表,对 Owner 信息和 addresses 列表进行编辑,最后将所有更改在一个事务中一起提交到数据库, 并且只能在用户确定保存主实体(Owner) 时才保存。

一对多组合

在本指南中会演示组合关系的不同用法。组合可以是 ONE-TO-MANY ,也可以是 ONE-TO-ONE。在应用程序中甚至可以使用多级嵌套组合。

随着 CUBA 7 的发布,CUBA 应用程序不再限制嵌套层数。但是,如果嵌套层数太多,复杂的界面操作常常会使用户迷惑。因此, 建议不要使用太多层次的嵌套组合。

一对多: 一层嵌套

使用 Owner 和 the Address 实体作为一对多组合的示例:

composition domain model one level
  • 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 视图,这是因为 Addressesowner 属性只在 Addresses 创建时设置并且后续不会更改,所以就不需要加载它。

  • owner-edit.xml - Owner 编辑界面的XML描述中为 Owner 实例定义了一个数据容器,同时也包含了一个嵌套的 addresses 集合容器。 XML描述中也包含一个用于管理 Address 实例的表格。

  • address-edit.xml - Address 实体的标准编辑界面。

最终,Owner 实例的编辑过程如下:

one level owner editor address composition

Owner 编辑界面显示了一个 addresses 列表。

修改过的 Address 实例并没有保存到数据库, 只是提交到了 Owner 编辑界面的 DataContext 。 用户可以创建新的实例,也可以删除已有实例。所有的更改都会提交到 DataContext。 在用户点击 Owner 编辑界面上的 OK 按钮时,修改过的 Owner 实例和 Address 实例会被提交给 dataManager.commit() 方法,并在同一个事务中保存到数据库。

一对多:两层嵌套

组合也可以有多级。下面的示例展示一个两级组合的示例。在宠物诊应用中有存储宠物健康记录的需求。PetHealthRecord 关系是另外一个组合, HealthRecord 也一个组合属性用于存储健康记录的附件信息。

composition domain model two levels
  • 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+ 不再限制组合关系的嵌套层数,这意味着可以使用三级(或更多)级的嵌套。示例中的两级嵌套可以继续扩展: OwnerPetHealth RecordHealth Record Attachment ,事实中示例程序中已经这样做了。 也可以在一个组合链中联合使用一对多和一对一组合。

要谨慎使用多级组合,因为它会为用户带来一些困惑,用户常常会分辨不出当前在嵌套层次的哪一级。

一对一组合

除了上面介绍的 一对多 组合,CUBA 平台中也允许定义 一对一 组合。 在一个实体仅有一个对应的子实体时这种类型的组合就很有用。宠物诊所中的示例场景:

宠物谁的的雇员需要管理。因此定义了一个 Employee 实体。每个 Employee 实例有一个关联的 EmployeeRecord 实体,用于存储这个雇员工作相关的信息。

composition one to one

在 UI 层面,有两种可能的方式处理一对一组合。通常,一对一组合在主实体的表单中显示为一个单一入口。通过这个入口打开一个独立的界面来编辑子实体的详细信息。但是也可以将主、子实体的信息显示在一个界面,并在一个界面中进行编辑。下面会展示这两种方式。

使用详细界面的的一对一关系

  • employee-edit.xml - 雇员实体编辑界面描述。为了加载嵌套的实例,根数据容器使用了 Employee 实体的 employee-with-employee-record-view 视图。

employee-edit.xmlemployeeRecord 属性被定义为 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>

最终,雇员编辑界面的操作流程如下:

composition one to one editor open action

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

选择器控件上显示了 EmployeeRecord 实体的实例名:

composition one to one editor open action instance name

当用户点击雇员编辑界面上的 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 实例关联起来:

EmployeeSingleEditorEdit.java
@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

现在,可以创建两个关联的实体并且在同一个界面中对其进行编辑。

composition one to one single editor

总结

本指南描述了数据建模中的组合关系。组合对具有主从关系的两个实体很有价值,组合关系表示子实体不能脱离主实体独立存在。

组合结构存在两种模式: 1:1和1:N。更进一步, 组合可以嵌套多级。对应的UI界面会以特殊的方式来处理组合关系。组合关系中的子实体总是与主实体一起保存。