值属性映射
前文说过,实体(和值对象)可以拥有两种类型的属性:
- 值属性
Attribute
:类型为简单值或值对象,或它们的某种类型的集合/数组。它们是实体的内在组成部分。 - 关联属性
Association
:类型为实体,或实体的某种类型的集合/数组。它们代表实体的外在关系。
区分这两者非常关键。
值属性可以是单值的,也可以是多值的。下面分别论述。
一、单值值属性映射
单值的值属性在数据库中没有自己对应的表,它们的值被持久化到实体对象对应的数据表的列中。
单值值属性分类两类:
1. 简单值属性
简单值属性用@Basic
逻辑注解,包括各种基本类型及其对象包装类型、日期/时间类型,字符串,枚举,布尔值等等。
@Basic
是默认的注解,意味着需要持久化的简单值属性可以标注、也可以不标注@Basic
注解。
@Basic
注解有一个optional
属性,指定字段是否接受空值。
单值的简单值属性可以添加@Column
物理注解,用来指定映射的数据列名称、是否接受空值、是否唯一等等。
下面是一些例子:
@Entity
@Table(name = "buyers")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(discriminatorType = DiscriminatorType.STRING)
public abstract class Buyer extends BaseEntity {
@Basic(optional = false)
@Column(name = "buyer_name", nullable = false, unique = true)
private String name;
...
}
2. 值对象属性
值对象属性用@Embedded
逻辑注解。
如果值对象类已经注解为@Embeddable
,则无需再在实体属性上添加@Embedded
注解。
如果值对象本身是多属性的,那么,它的各个属性会扁平化后存储到实体类对应的数据表的多个列上。例如订单Order
对象对应到数据库的orders
数据表,而订单类有个代表送货地址的shippingAddress
属性,其类型是值对象Address
,它本身有省、市、详细地址等多个属性,那么Address
的每个属性值都会作为单独的列存储到orders
数据表中。
下面是一些例子:
@Entity
@Table(name = "orders")
public class Order extends BaseEntity {
@Embedded
private Address shippingAddress;
}
可以在值对象中为每个属性定义映射元数据。和实体一样,值对象可以有值属性和关联属性,单值和多值的都可以。
因为值对象中的每个属性会扁平化地存放在它所属的实体对应的数据表中,那么就可能导致一个问题:值对象中的属性持久化列名可能和它所属实体的其他简单值属性或其他值对象属性的下级属性列名同名,导致冲突。这个时候可以通过在值对象属性上添加@AttributeOverride
逻辑注解,将值对象中的属性映射到另外的列名字,避免冲突。下面是个例子:
@Entity
@Table(name = "orders")
public class Order extends BaseEntity {
@Embedded
@AttributeOverride(name = "value", column = @Column(name = "total_price"))
private Money totalPrice;
}
@Embeddable
public class Money {
private BigDecimal value = BigDecimal.ZERO;
}
根据Money
类的定义,它含有一个名为value
的属性,默认会映射到所属实体表的value
列。我不喜欢它在订单表中以value
为列名(虽然在这个例子中没造成任何命名冲突),所以在Order
类的映射中,我通过@AttributeOverride
注解,将属性value
重新映射到total_price
列。这样表达性好得多。
二、多值值属性映射
多值值属性,是一个List
、Set
、SortedSet
、Map
或数组类型的属性,其中包含的元素类型是简单值或值对象。
多值值属性统一用@ElementCollection
逻辑注解标识。并且在数据库中用单独的数据表存放其内容。这个表的名字和其他属性可以用@CollectionTable
物理注解进行映射。
1. 简单值的多值属性
下面是简单值的多值属性的例子:
@Entity
@DiscriminatorValue("P")
public class PersonalBuyer extends Buyer {
@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<>();
}
这个例子中:
- 用
@ElementCollection
逻辑注解标识这是个多值的值属性。 - 用
@CollectionTable
物理注解指明用来存储内容的数据表名称是contact_infos
,这个表中有一个外键列buyer_id
,指向实体所在的buyers
表的主键。。 - 这个多值属性是一个
Map
,所以要分别针对它的key
和value
指定映射注解。 - 这个
Map
的key
是枚举类型ImType
。通过@MapKeyColumn
物理注解指定key
在数据库中对应的列名是im_type
。通过逻辑注解@MapKeyEnumerated(EnumType.STRING)
告诉JPA
,在数据库中存储枚举的字符串名称而不是序号。 - 这个
Map
的value
是个字符串。通过物理注解@Column(name = "im_value")
指定表中存储value
的列名为im_value
。 - 最终结果是:用数据库表
contact_infos
来存储集合的内容。其中im_type
列存储Map
的key
,im_value
列存储Map
的value
,还有一个外键列buyer_id
,指向值对象所属的实体的表buyers
的主键列。 - 上述注解中,只有
@ElementCollection
逻辑注解是必须的,其余各项都有默认值。
2. 值对象的多值属性
下面是多值的值对象属性的例子:
@Entity
@Table(name = "buyers")
public abstract class Buyer extends BaseEntity {
@ElementCollection
@CollectionTable(name = "shipping_addresses", joinColumns = @JoinColumn(name = "buyer_id"))
private Set<Address> shippingAddresses = new HashSet<>();
}
在这个例子中:
- 用
@ElementCollection
逻辑注解标识这是个多值的值属性。 - 用
@CollectionTable
物理注解指明用来存储Address
内容的数据表名称是shipping_addresses
,值对象Address
的各个属性分别存储到数据表shipping_addresses
各自的列中。shipping_addresses
表中还有一个外键列buyer_id
,指向实体所在的buyers
表的主键。 - 这个集合属性的类型是
Set
。这表明集合中的元素不可重复且不保证排列顺序。Set
根据其中的元素的equals()
方法来判断两个元素是否代表相同的元素。根据Set
的定义,不可以包含相同的元素。