数据建模: 多对多关系

多对多关系实现一个表中的多条记录与另一个表的多条记录关联。关联表中将存储两个关联实体的主键,关联表中也可以包含其它附加列。

根据是否需要在关联表中包含附加列,多对多关系的实现有两种方式,即添加额外实体或不添加额外实体。下面的示例演示这两种方式。

将要构建的内容

这个向导对 CUBA 宠物诊所 示例进行了增强,以演示多对多关系的用法。以下用例会重点演示:

  • Vet ←→ Specialty 被设计为多对多关系,相应的UI也做了更新

  • Pet ←→ InsuranceCompany 被设计为间接的多对多关系,用了一个明确定义的实体来将两个实例建立关联,同时在这个实体中存储保险的 “有效期” 信息。

开发环境要求

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

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

示例: CUBA 宠物诊所

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

这个应用程序的领域模型如下:

领域模型

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

直接的多对多关系

这里有一个宠物诊所应用中的一个直接多对多关系的示例:

direct many to many

一个 兽医(Vet) 可以拥有多项 专长(Speciality),另一方面一项专长也可以关联给多个兽医。

在数据库层面 ,这种关系表现为一个关联表,但是实体模型设计层面 ,没有 明确定义的关联实体。

在 Studio 提供的实体设计器中,给 specialties 属性做以下设置: Attribute type - ASSOCIATION, Cardinality - MANY_TO_MANY . Vet 将被标记为关系的拥有方,同时 Studio 也会提示在 Specialty 实体中创建一个对应的 vets 属性作为关系反向引用。

Vet.java - Vet 实体包含一个名称为 specialties 、 被标记为多对多关系的列表属性。

Vet.java
@JoinTable(name = "PETCLINIC_VET_SPECIALTY_LINK",
        joinColumns = @JoinColumn(name = "VET_ID"),
        inverseJoinColumns = @JoinColumn(name = "SPECIALTY_ID"))
@ManyToMany(mappedBy = "")
protected Set<Specialty> specialties;

Specialty.java - Specialty 现在包含了一个标记为多对多关系的列表属性 vets ,设置如下: Attribute type - ASSOCIATION, Cardinality - MANY_TO_MANY

Specialty.java
@JoinTable(name = "PETCLINIC_VET_SPECIALTY_LINK",
        joinColumns = @JoinColumn(name = "SPECIALTY_ID"),
        inverseJoinColumns = @JoinColumn(name = "VET_ID"))
@ManyToMany
protected List<Vet> vets;

Specialty 也默认被标记为关系的拥有方,这样在两边都可以修改关联的集合了。

  • views.xml - 兽医编辑界面使用的视图: vet-with-specialties 包含了关联属性: specialties(专长),这个属性使用了 _minimal 视图。 视图 specialty-with-vets 同样也包含了 vets(兽医) 属性 。

  • vet-edit.xml - 兽医编辑界面的 XML 描述中为 Vet 实例定义了一个数据源,同时也为兽医的专长定义了一个嵌套数据源。界面上还有一个用于显示兽医专长的表格,表格上有 添加移除 操作。

  • specialty-edit.xml - 专长编辑界面的 XML 描述中为 Specialty 实例定义了一个数据源,同时也为对应的兽医定义了一个嵌套数据源。界面上同样有表格和 添加移除 操作。

所以 ,VetSpecialty 编辑器是完全对称的。

使用关联实体的间接多对多关系

多对多关系都是使用一个关联表实现的,但是不是必须创建一个实体去对应这个表。如果需要在关联表中存储一些额外属性时,那么就需要创建一个关联实体。

下面是一个在宠物诊所示例中关于间接多对多关系的示例:

indirect many to many

一个 Pet(宠物) 每年可以在不同的保险公司投保,另一方面,InsuranceCompany(保险公司) 也可以为不同的的宠物提供保险服务。

在数据库中,这种关系表现为一个关联表,并且有一个显式定义的实体作为关联实体。投保关系中也要记录保险公司给宠物提供的保险范围。所以实体上有 validFromvalidUntil 属性。

在 Studio 提供的实体设计中, 为 insuranceMemberships 属性进行以下设置: Attribute type - COMPOSITIONCardinality - ONE_TO_MANY

Pet.java - Pet 实体包含一个一对多的 InsuranceMembership 实例组合。

Pet.java
@Composition
@OnDelete(DeletePolicy.CASCADE)
@OneToMany(mappedBy = "pet")
protected List<InsuranceMembership> insurancesMemberships;
在 Studio 提供的实体设计器中给 memberships 属性进行以下设置: Attribute type - COMPOSITION, Cardinality - ONE_TO_MANY

InsuranceCompany.java - InsuranceCompany 实体包含一个一对多的 InsuranceMembership 实例组合。

InsuranceCompany.java
@Composition
@OnDelete(DeletePolicy.CASCADE)
@OneToMany(mappedBy = "insuranceCompany")
protected List<InsuranceMembership> memberships;

InsuranceMembership.java - 现在, InsuranceMembership 实体包含了两个多对一引用: petinsuranceCompany.

InsuranceMembership.java
@NotNull
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "PET_ID")
protected Pet pet;

@NotNull
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "INSURANCE_COMPANY_ID")
protected InsuranceCompany insuranceCompany;

@Temporal(TemporalType.DATE)
@NotNull
@Column(name = "VALID_FROM", nullable = false)
protected Date validFrom;

@Temporal(TemporalType.DATE)
@Column(name = "VALID_UNTIL")
protected Date validUntil;
  • views.xml - 宠物编辑界面使用的 pet-with-owner-and-type-and-memberships 视图含有 insuranceMemberships 组合属性(引用 InsuranceMembership 关联实体),其中包含 insuranceCompany 和额外的有效期信息:validFromvalidUntil

    insuranceCompany-with-memberships 视图遵循相同的逻辑:含有 memberships 组合属性(引用 InsuranceMembership 关联实体 ), 其中包含了 pet 和额外的有效期信息: validFromvalidUntil

宠物编辑界面的 XML 描述中为 Pet 实例定义了一个数据容器,为它的 insuranceCompany (通过 InsuranceMembership 关系) 实例定义了一个嵌套数据容器。 界面上有一个用于显示投保关系的表格和一个可以跳过`InsuranceMembership` 编辑器直接选择 InsuranceCompany 的自定义操作。

最后 ,编辑 InsuranceMembership 实例的过程如下:

Pet 编辑界面显示一个保险公司列表和投保关系的有效期范围。

用户可以点击 Add Insurance 按钮, 打开查找 InsuranceCompany 的界面,用户可以选择一个已有的保险公司或者创建一个新的保险公司。当前用户选择了一个保险公司后,一个新的 InsuranceMembership 实例会被 创建,新的实例使用了默认的有效期范围。这个实例不会被直接保存到数据库, 只是添加到了 Pet 编辑界面的 insurancesMembershipsDc 数据容器。

当从宠物编辑界面通过上述方式进行了多对多关系的创建后,会通过 InsuranceCompany 编辑界面 创建或更新一个 InsuranceCompany 实例。 保险公司实例的创建或更新是完全独立地被保存到数据库。用户可以创建一个新的保险公司同时删除一个已有的保险公司,对保险公司的所有修改都是在一个独立的事务中保存到数据库。

当用户点击宠物编辑界面上的 OK 按钮,修改后的 Pet 实例和所有修改的 InsuranceMembership 实例会提交到中间件的 DataManager.commit() 方法并在同一个事务中保存到数据库。

总结

在这个数据建模指南中我们演示了多对多关系。多对多关系对于关系的两边都需要链接另一边的多个条目时非常有用,上述的 Vet ←→ Specialty 用例即是这种情况。如果业务场景需要,可以通过添加额外的信息对这个关系进行增强,上面展示的为 InsuranceMembership 实体添加 validFrom & validUntil 属性的用例即是这种场景。