写点什么

原创 | 使用 JPA 实现 DDD 持久化 -O/R 映射元数据 - 值和关联的比较 + 继承映射

发布于: 2020 年 12 月 18 日
原创 | 使用JPA实现DDD持久化-O/R映射元数据-值和关联的比较+继承映射

值和关联的比较



值没有独立的生命周期,完全从属于所属的实体,是实体的内在组成部分。它们(无论单值还是多值)随着实体的保存而自动保存,亦随着实体的删除而自动删除。因此,比起实体来说,管理值对象简单的多。



举例来说,上面的例子中,实体PersonalBuyer拥有一个集合类型为Map的多值值属性imInfos,用于记录买家的各种即时消息应用(例如QQ、微信、WhatsappSkype等)的账号。当我们往这个集合中添加或删除一些条目,或者变更了部分条目的内容时,只需要直接保存PersonalBuyer实体,这个集合就会自动保存,不需要区分那些条目是新增的,哪些条目已经删除,哪些条目又是原有的条目做了内容更改。如果这个集合是个实体的集合,就不会有这样的便利。你必须分别针对增/删/改的条目分别处理,还要正确地指定关联,一不小心还可能会在数据库中留下很多孤儿对象。



另一个区别是删除。如果属性是单值或多值的简单值/值对象。那么,删除实体的时候,这些单值或多值的值对象也会自动删除,不需要分别处理。如果属性是关联属性,情况将大大不同。由于每个实体都有独立的生命周期,删除一个实体不会自动删除它关联的实体(除非添加某些特殊注解),甚至会由于违反引用完整性而拒绝删除。这时候你需要先删除所有引用的实体,才能删除被引用的实体。



如果可能,尽量将对象建模为值对象而不是实体。



最重要的一点是要记住:值(包括简单值和值对象)属于内部状态,而关联属于外部关系



继承映射



继承是面向对象的本质特征之一。JPA支持继承映射,这是JPA这类以对象为中心的持久化方案相对于JDBCMyBatis等以数据为中心的持久化方案的关键优势之一。被继承的基类可以是实体,也可以不是实体。下面分别论述,



实体继承



领域模型之中很多实体之间存在着继承关系。典型例子是银行账户,就有信用账户和储蓄账户之分。两者之间即有共性,又有个性。因此可以建模为一个基类(账户)和两个子类(信用账户和储蓄账户)。在基类中定义共同的属性和行为,而在子类中定义特有的属性和机制。



在本范例项目中,买家(Buyer)也有两种类型:个人买家(PersonalBuyer)和机构买家(OrgBuyer)。前者是自然人而后者是政府机构或企事业单位。这里也存在着继承关系:个人买家(PersonalBuyer)和机构买家(OrgBuyer)两个子类继承共同基类Buyer



@startuml
abstract class BaseEntity {
- id: int
- version: int
}
abstract class Buyer <<Entity>> {
- name: String
- mobileNo: String
- wiredNo: String
- email: String
}
class OrgBuyer <<Entity>> {
- businessLicenseNo: String
- taxNo: String
}
class ContactInfo <<ValueObject>> {
- name: String
- gender: Gender
- mobileNo: String
- email: String
}
class Address <<ValueObject>> {
- province: String
- city: String
- detail: String
- receiver: String
- receiverPhone: String
}
BaseEntity <|- Buyer
Buyer <|- OrgBuyer
Buyer <|- PersonalBuyer
Buyer *--> "shippingAddresses*" Address
OrgBuyer *--> ContactInfo
@enduml



继承映射的代码范例:



@Entity
@Table(name = "buyers")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(discriminatorType = DiscriminatorType.STRING)
public abstract class Buyer extends BaseEntity {
private String name;
private String mobileNo;
private String wiredNo;
private String email;
@ElementCollection
@CollectionTable(name = "shipping_addresses", joinColumns = @JoinColumn(name = "buyer_id"))
private Set<Address> shippingAddresses = new HashSet<>();
}
@Entity
@DiscriminatorValue("P")
public class PersonalBuyer extends Buyer {
@Enumerated(EnumType.STRING)
private Gender gender;
@ElementCollection
@CollectionTable(name = "contact_infos", joinColumns = @JoinColumn(name = "buyer_id"))
@MapKeyEnumerated(EnumType.STRING)
@MapKeyColumn(name = "im_type")
@Column(name = "im_value")
private Map<ImType, String> imInfos = new HashMap<>();
}
@Entity
@DiscriminatorValue("O")
public class OrgBuyer extends Buyer {
@Column(name = "business_license_no")
private String businessLicenseNo;
private String taxNo;
@AttributeOverrides({
@AttributeOverride(name = "name", column = @Column(name = "contact_name")),
@AttributeOverride(name = "gender", column = @Column(name = "contact_gender")),
@AttributeOverride(name = "mobileNo", column = @Column(name = "contact_mobile_no")),
@AttributeOverride(name = "email", column = @Column(name = "contact_email"))
})
private ContactInfo contactInfo;
}



说明如下:



  • 通过在抽象基类Buyer上添加Inheritance逻辑注解建立一棵继承树。这个注解有一个属性strategy,用来定义继承策略。此处采用的策略是单表策略InheritanceType.SINGLE_TABLE,就是将所有基类子类的共有属性和特有属性全部映射到同一张数据表。

  • 可选的@DiscriminatorColumn物理注解定义鉴别列。由于整棵继承树都持久化到同一张表,JPA通过在表中添加一个鉴别列来区分存储的是具体哪一个子类。这个注解的namediscriminatorType属性分别定义鉴别列的名称和类型。默认的列名是DTYPE而类型是DiscriminatorType.STRING

  • 每个子类分别通过@DiscriminatorValue逻辑注解定义本类型对应的鉴别列的值。在本例子中,PersonalBuyer的鉴别列存储的值是字符串POrgBuyer的鉴别列存储的值是字符串O

  • 基类和每个子类都必须分别添加逻辑注解@Entity@Entity注解不可继承。

  • 在基类Buyer中定义共同属性namemobileNowiredNoemailshippingAddresses,由所有的子类继承。在子类PersonalBuyer中定义个人买家特有属性genderimInfos;在子类OrgBuyer中定义机构买家特有属性businessLicenseNotaxNocontactInfo

  • 继承的层级理论上是没有限制的。当然现实中一般只是两层,最多不要超过三层。



通过继承映射,JPA支持多态关联多态查询



多态关联



多态关联是指一个实体关联到另一个/一组实体,而后者的类型(在多值的情况下是元素类型)是一个实体基类。



本项目中,每个订单Order关联到一个买家Buyer,这个买家可能是个人买家也可能是机构买家。



@Entity
@Table(name = "orders")
public class Order extends BaseEntity {
@ManyToOne
private Buyer buyer;
public Buyer getBuyer() {
return buyer;
}
public void setBuyer(Buyer buyer) {
this.buyer = buyer;
}
}



当创建订单的时候,如果通过setBuyer()方法传递给订单实体的是一个个人买家,那么将来获取订单的时候,通过getBuyer()方法获取到的买家就是个人买家类型。机构买家同理。JPA自动处理与类型有关的一切,不需要开发者编写多余的代码。



多态查询



多态查询是指以实体基类或子类为查询目标的查询。



我们可以针对基类或个别子类分别做查询。以JPA查询语言jpql为例(jpql将在后面的章节中详细论述)。



针对个人买家的查询例子:



select b from PersonalBuyer b where b.gender = :gender



针对机构买家的查询例子:



select b from OrgBuyer b where b.businessLicenseNo = :businessLicenseNo



针对买家抽象基类的查询的例子:



select b from Buyer b where name = :name



最后一个查询既包含符合查询条件的个人买家,也包含符合条件的机构买家。



由机构买家所下的订单的查询例子:



select o from Order o join o.buyer b where TYPE(b) = OrgBuyer



这个查询将查找出买家类型为机构买家的所有订单。



非实体继承



非实体继承是指子类是实体,但基类不是实体的继承类型。



如果基类被注解为@MappedSuperclass,那么这个基类中定义的所有持久化属性,都会分别持久化到所有实体子类的相应的表的字段中(每个实体子类对应的表中都包含这些属性映射的列)。本项目中,所有实体都继承自共同基类BaseEntity,通过这个基类定义了所有实体的一些共同属性。其定义如下:



@MappedSuperclass
public abstract class BaseEntity implements Serializable {
@Id
@GeneratedValue
private int id;
@Version
private int version;
private LocalDateTime created;
@Column(name = "last_updated")
private LocalDateTime lastUpdated;
@Transient
private boolean isNew = true;
}



上面的BaseEntity由于有@MappedSuperclass注解,它的idversioncreatedlastUpdated属性都会作为持久化属性被所有的实体子类继承。isNew属性由于注解为@Transient,不会被子类持久化。



@MappedSuperclass注解的基类包含的持久化属性,既可以是值属性,也可以是关联属性;既可以是单值属性,也可以是多值属性。除了不能作为查询和关联目标之外,几乎可以作为实体一样看待。



如果基类没有添加@MappedSuperclass注解,那么基类中定义的任何属性都不会持久化到数据库,即使在属性级别添加了映射元数据也没有任何作用。



详细内容请戳这里↓↓↓



原创 | 使用JPA实现DDD持久化-O/R映射元数据-值和关联的比较+继承映射



这一节就讲到这里,下一节我们讲"启动JPA程序+通过JPA原生API访问数据"



如果觉得有收获,点个【赞】鼓励一下呗!





发布于: 2020 年 12 月 18 日阅读数: 11
用户头像

高级架构师,技术顾问,交流公号:编程道与术 2020.04.28 加入

杨宇于2020年创立编程道与术,致力于研究领域分析与建模、测试驱动开发、架构设计、自动化构建和持续集成、敏捷开发方法论、微服务、云计算等顶尖技术领域。 了解更多公众号:编程道与术

评论

发布
暂无评论
原创 | 使用JPA实现DDD持久化-O/R映射元数据-值和关联的比较+继承映射