Pro JPA2 第十章(高级对象-关系映射)
10.1 表和列名
在前面的章节中,已经显示了表和列的名称位大写标识符,这么做的理由是:首先,这有助于将它们与Java标识符区分开来;其次,因为SQL标准定义了未分割的数据库标识符不区分大小写,而往往以大写形式来表示.
每当指定或者默认表和列名时,将完全把指定或默认的标识符字符串传递给JDBC驱动程序.@Table(name="employee") @Table(name="Employee") @Table(name="EMPLOYEE")
上述三个写法都在数据库中显示为同一个表.
如果有些数据库的名称打算区分大小写,那么必须显式地分割.分割的方法是使用必须进行转义的第二组双引号来围绕该标识符.@Table(name="\"Employee\"") @Table(name="\"EMPLOYEE\"")
当使用XML映射文件时,标识符也是通过在标识符名称中包括引号来分割.
<column name=""ID"" /> <column name=""Id"" />
10.2 复杂的嵌入对象
10.2.1 高级嵌入映射
嵌入对象可以嵌入其他的对象,具有基本类型或可嵌入类型的元素集合,以及与实体存在关系.在这个假设直下,嵌入在其他嵌入对象中的对象仍然依赖于所嵌入的实体,是完全可能的.@Embeddable @Access(AccessType.FIELD) public class ContactInfo { @Embedded private Address residence; @ManyToOne @JoinColumn(name="PRI_NUM") private Phone primaryPhone; @ManyToMany @MapKey(name="type") @JoinTable(name="EMP_PHONES") private Map<String,Phone> phones; } @Entity public class Phone { @Id String num; @ManyToMany(mappedBy="contactInfo.phones") List<Employee> employees; String type; }
10.2.2 重写嵌入关系
@Entity public class Customer { @Id int id; @Embedded @AttributeOverride(name="address.zip",column=@Column(name="ZIP")) @AssociationOverrides({ @AssociationOverride(name="primaryPhone",joinColumns=@JoinColumn(name="EMERG_PHONE")), @AssociationOverride(name="phones",joinTable=@JoinTable(name="CUST_PHONE")) }) private ContactInfo contactInfo; }
10.3 复合主键
在某些情况下,实体的主键或标识符必须由多个字段组成,或者从数据库的角度来看,表中的主键是由多个列组成.
取决于我们所希望的实体类的结构,存在两个选项可用于在实体中拥有复合主键.它们都要求使用一个单独的类来包含主键字段,成为主键类(primary key class).主键类必须包含equals()和hashCode()方法定义,以便能够由持久化提供程序存储和作为键.它们还必须是公共的,实现了Serializable,以及拥有一个无参构造函数.10.3.1 id类
第一个也是最基本的主键类类型是id类(id class). 使用@Id注解来标记组成实体主键的每个字段.通过在实体类定义中使用@IdClass注解来单独定义主键类,并且使其与实体相关联.@Entity @IdClass(EmployeeId.class) public class Employee { @Id private String country; @Id @Column(name="EMP_ID") private int id; private String name; private long salary; }
主键类必须包含在名称和类型方面均匹配实体中主键特性的字段或属性.
public class EmployeeId implements Serializable { private String country; private int id; public EmployeeId(){} public EmployeeId(String country,int id) { this.country = country; this.id = id; } public String getCountry() { return country; } public int getId() { return id; } public boolean equals(Object o) { return ((o instanceof EmployeeId) && country.equals(((EmployeeId)o).getCountry())&& id == ((EmployeeId)o).getId()); } public int hashCode(){ return country.hashCode() + id; } }
在具有Id类的实体上调用一次主键查询
EmployeeId id = new EmployeeId(country,1); Employee emp = em.find(Employee.class,id);
10.3.2 嵌入id类
若一个实体包含了与主键类具有相同类型的单个字段,则称之为采用了嵌入id类(embedded id class).嵌入id类知识一个恰好是由主键组件组成的嵌入对象.使用@EmbeddedId注解来指示它不仅仅是一个常规的嵌入对象,而且还是一个主键类.当采用这种方法时,类中将不存在@Id注解,也不能使用@IdClass注解.@Embeddable public class EmployeeId { private String country; @Column(name="EMP_ID") private int id; public EmployeeId(){} public EmployeeId(String country,int id) { this.country = country; this.id = id; } } @Entity public class Employee { @EmbeddedId private EmployeeId id; private String name; private long salary; public Employee(){} public Employee(String country,int id) { this.od = new EmployeeId(country,id); } public String getCountry(){ return id.getCountry(); } public int getId(){ return id.getId(); } }
在查询中引入嵌入id类
public Employee findEmployee(String country,int id) { return (Employee) em.createQuery("SELECT e FROM Employee e WHERE e.id.country = ?1 AND e.id.id = ?2").setParameter(1,country).setParameter(2,id).getSingleResult(); }
10.4 派生标识符
当一个实体的标识符包括指向另一个实体的外键时,称之为派生标识符.因为包含派生标识符的实体取决于另一个实体作为其标识符,因此称第一个实体为依赖实体,它所依赖的实体是来自依赖实体的多对一或一对一关系的目标,称之为父实体.下例中,DEPARTMENT表是父实体表,PROJECT表是依赖实体.
没有主键,依赖对象就不能够存在,并且由于它的主键包含指向父实体的外键,因此应该明确在没有建立与父实体的关系时,新的依赖实体不能够进行持久化.修改现有实体的主键是未定义的,因此作为派生标识符一部分的一对一或多对一关系也是不可变的.一旦依赖实体已经持久化,或已经存在,就不应把它重新分配到一个新的实体.- 10.4.1 派生标识符的基本规则
- 依赖实体可能又多个父实体,即:派生标识符可能包括多个外键.
- 依赖实体在持久化之前,必须设置其与父实体的所有关系.
- 如果实体类有多个id特性,那么它不仅必须使用id类,而且对于实体的每个id特性,在id类中都必须存在一个对应的相同名称的特性.
- 实体的id特性可能是一个简单类型,或者是一个实体类型,作为多对一或一对一的关系的目标.
- 如果实体的id特性是一个简单类型,那么在id类中的匹配特性的类型必须是相同的简单类型
- 如果实体的id特性是关系,那么在id类中匹配特性的类型与关系的目标实体的主键类型为同一类型
- 如果依赖实体的派生标识符是嵌入id类的形式,那么id类中每个表示关系的特性都应该被一个在对应的关系特性只上的@MapSid注解所引用
10.4.2 共享主键
一个简单但不太常见的情况是,派生标识符由作为关系外键的单个特性构成.@Entity public class EmployeeHistory { @Id @OneToOne @JoinColumn(name="EMP_ID") private Employee employee; }
EmployeeHistory和Employee的主键类型属于同一类型,所有如果Employee有一个简单的整数标识符,那么EmployeeHistory的标识符也会是一个整数.如果Employee有一个复合主键,无论是id类或嵌入id类,那么Employeehistory将共享相同的id类.问题在于,这将会影响id类规则,因为id类的每个特性都应该在实体中存在与其匹配的特性.由于在父实体和依赖实体之间共享id类的事实,因此这是规则的例外.
有时候,我们也许会希望实体包含一个主键特性和一个关系特性,两个特性都映射到表中的相同外键列.即使在实体中主键特性是不必要的,有些人也可能想要单独地定义它以方便访问.但是其实是不需要这样重复定义的.@Id注解放在标识符特性上,同事@MapsId注解了关系特性,以只是它也正在映射id特性.@Entity public class EmployeeHistory { @Id int empId; @MapsId @OneToOne @JoinColumn(name="EMP_ID") private Employee employee; }
10.4.3 多个映射特性
一种更为常见的情况可能是,依赖实体有一个标识符,它不仅包括关系,而且还包括一些自己的状态.@Entity @IdClass(ProjectId.class) public class Project { @Id private String name; @Id @ManyToOne private Department dept; }
复合标识符意味着还必须使用@IdClass注解来指定主键类.回顾一下规则:对于实体的每个id特性,主键类必须具有一个匹配的命名特性,而且通常还必须是同一类型的特性.然后,此规则仅适用于当特性是简单类型而不是实体类型时.如果@Id正在注解关系,那么关系特性将是某种目标实体类型,并且规则扩展为主键类特性必须与目标实体的主键类型相同.这以为着上例中指定的ProjectId类必须有一个String类型的名为name的特性,同事另一个名为dept的特性将与Department的主键具有相同的类型.
public class ProjectId implements Serializable { private String name; private DeptId dept; public ProjectId(){} public ProjectId(DeptId deptId,String name) { this.dept = deptId; this.name = name; } } public class DeptId implements Serializable { private int number; private String country; public DeptId(){} public DeptId(int number,String country) { this.number = number; this.country = country; } }
10.4.4 使用EmbeddedId
当一个或另一个实体(或两者)使用@EmbeddedId时,也可能会有派生标识符.下例所示当使用嵌入id类时,如何在Project类中映射派生标识符.使用@MapSid(“dept”)注解关系特性,指示它也正在为嵌入id类的dept特性指定映射.@Entity public class Project { @Embedded private ProjectId id; @MapsId("dept") @ManyToOne @JoinColumns({ @JoinColumn(name="DEPT_NUM",referencedColumnName="NUM"), @JoinColumn(name="DEPT_CTRY",referencedColumnName="CTRY") }) private Department department; } @Embeddable public class ProjectId implements Serializable { @Column(name="P_NAME") private String name; @Embedded private DeptId dept; } @Entity public class Department { @EmbeddedId private DeptId id; @OneToMany(mappedBy="department") private List<Project> projects; } @Embeddable public class DeptId implements Serializable { @Column(name="NUM") private int number; @Column(name="CTRY") private String country; }
- 10.4.1 派生标识符的基本规则
10.5 高级映射元素
10.5.1 只读映射
@Entity public class Employee { @Id @Column(insertable = false) private int id; @Column(insertable = false,updateable = false) private String name; @Column(insertable = false,updateable = false) private long salary; @ManyToOne @JoinColumn(name="DEPT_ID",insertable = false,updateable = false) private Department department; }
10.5.2 可选性
如果存在元数据允许数据库列为null或者要求他们具有之,那么可以使用@Basic,@ManyToOne和@OneToOne注解中的optional元素.
当把optional元素指定为false时,该属性或字段不能为null.相反,如果把optional元素指定为true,那么该属性或字段可以为null.@Entity public class Employee { @ManyToOne(optional = false) @JoinColumn(name="DEPT_ID",insertable = false,updateable = false) private Department department; }
- 10.6 高级关系
略 - 10.7 多个表
略 10.8 继承
10.8.1 类层次结构
因为本书讲述的是Java持久化API,所以开始讨论集成的第一个也是最明显的地方是在Java对象模型中.毕竟,实体是对象,应该能够从其他实体继承状态和行为.
当一个实体从它的实体超类中继承状态时意味着什么?在数据模型中,它可能暗示了不同的东西,但是在Java模型中,仅仅意味着当实例化一个子类实体时,它拥有本地定义的状态及其所继承的状态的自身版本或者副本,所以这些状态都是持久性的.
继承类的层次结构:映射超类
Java持久化API定义了一种特殊的类,称为映射超类,它作为一个实体超类相当有用.映射超类提供了一个非常方便的类,可用它来存储实体可以继承的共享状态和行为,但它本身不是一个持久化类,不具备实体的能力.不能对其进行查询,也不能作为关系的目标.在映射超类上不允许使用诸如@Table之类的注解,因为在它们只上所定义的状态仅适用于其实体的子类.
映射超类与实体相比,在某种程度上和抽象类与具体类的比较相同:它们可以包含状态和行为,但是不能作为持久化实体进行实体化.一个抽象类仅在与具体的子类相关时才有用,而一个映射超类只有在作为扩展它的实体子类所继承的状态和行为时有用.除了为继承他们的实体贡献状态和行为之外,它们在一个实体继承层次结构中不发挥作用.
在它们的类定义中,可以或者不可以把映射超类定义为抽象类型,但是把它们变为是几点 抽象Java类是好的做法.
应用到实体的所有默认的映射规则也适用于在映射超类中的基本状态和关系状态,所有映射超类的最大优点是能够定义部分共享状态,它们无须自己访问,不附加它的实体子类所添加的状态.如果您不确定是否需要使类成为一个实体或者一个映射超类,那么您只需问自己是否需要查询或访问一个仅公开为该映射超类实例的实例.这也包括关系,因为映射超类不能用作关系的目标.@Entity public class Employee { @Id private int id; @Temporal(TemporalType.DATE) @Column(name="S_DATE") private Date startDate; } @Entity public class ContractEmployee extends Employee { @Column(name="D_RATE") private int dailyRate; private int term; } @MappedSuperclass public abstract class CompanyEmployee extends Employee { private int vacation; } @Entity public class FullTimeEmployee extends CompanyEmployee { private long salary; private long pension; } @Entity public class PartTimeEmployee extends CompanyEmployee { @Column(name="H_RATE") private float hourlyRate; }
层次结构中的瞬态类
我们把实体层次结构中不是实体或映射超类的类称为瞬态类(transient class).实体可以通过映射超类直接或间接的扩展瞬态类.当实体从一个瞬态类继承时,在瞬态类中所定义的状态仍然在实体中继承,但不是持久性的.换句话说,实体将根据通常的Java规则为继承的状态分配空间,但是不会通过持久化提供程序管理其状态.在实体的声明周期期间将会有效地忽略它.public abstract class CachedEntity { private long createTime; public CachedEntity() { createTime = System.currentTimeMillis(); } public long getCacheAge() { return System.currentTimeMillis() - createTime; } } @Entity public class Employee extends CachedEntity { public Employee(){ super(); } }
- 抽象类和具体类
已经在映射超类的上下文中提到了抽象类和具体类的概念,但是没有进一步更详细地介绍实体和瞬态类.在继承树中的任何级别中,实体,映射超类或瞬态类既可以是抽象的,也可以是具体的,这是完全可以接受的.对于映射超类,使瞬态类在层次结构中成为具体类并不能真正地达到任何目的,因此作为一般规则应该避免它,以防止意外的开发错误和滥用.
一个实体是抽象类还是具体类的唯一区别是禁止实例化抽象类的Java规则.他们仍然可以定义持久化状态和行为,将由它们下边的具体实体子类来继承.
10.8.2 继承模型
JPA支持三种不同的数据表示.
当存在一个实体的层次结构时,它总是以实体类为根.注意映射超类没有作为层次结构中的级别进行计算,因为它们仅对它们下面的实体有贡献.根实体类必须通过使用@Inheritance注解来表示继承层次结构.单表策略
用于存储多个类的状态的最常见也是高性能的方法是定义一个单表,其包含在任何实体类长所有可能状态的超集,这就是单表策略.对于表示一个具体类的实例的任何给定的表行,可能有些列没有值,因为它们只应用于层次结构中的一个同级类(sibling class).
一般情况下,单表方法往往会更加浪费数据库表空间,但是它确实为多态查询和写操作都提供了峰值性能(peak performance).需要发出这些操作的SQL是简单和优化的,并且不需要联接.
为了给继承层次结构指定单表策略,使用@Inheritance注解来注释根实体类,并且把它的策略设置为SINGLE_TABLE.@Entity @Inheritance(strategy=InteritanceType.SINGLE_TABLE) public abstract class Employee { }
在下图中,可以看到Employee层次结构模型的单表表示.根据用于单表策略的表结构和架构体系结构,没有区分CompanyEmployee是否是一个映射超类或一个实体.
- 鉴别器列
上图中的EMP_TYPE被称为鉴别器列,并且通过@DiscriminatorColumn注解和已经了解的@Inheritance注解结合来映射它.该注解的name元素指定应作为鉴别器列的列名,如果没有指定,则默认为”DTYPE”
discriminatorType元素有三个参数INTEGER,STRING和CHAR,分别代表用数值,字符串,字符来区分鉴别器列的类型.默认为STRING. 鉴别器值
表中的每一行将在鉴别器列上有一个值,称为鉴别器值或类指示器(class indicator).用于指示存储在那一行中的实体类型.对每个具体的实体类都应该使用一个@DiscriminatorValue注解.在该注解中的字符串值指定了鉴别器值,当把类的实例插入到数据库时分配给它们.这将允许提供程序发出查询时识别类的实例.这个值应该与@DiscriminatorColumn注解的discriminatorType元素所指定或默认的类型相同.@Entity @Table(name="EMP") @Inheritance @DiscriminatorColumn(name="EMP_TYPE") public abstract class Employee { } @Entity public class ContractEmployee extends Employee { } @MappedSuperclass public abstract class CompanyEmployee extends Employee { } @Entity @DiscriminatorValue("FTEmp") public class FullTimeEmployee extends CompanyEmployee { } @Entity(name="PTEmp") public class PartTimeEmployee extends CompanyEmployee { }
单表继承的数据示例:
- 鉴别器列
联接策略
虽然联接继承在数据存储方面既直观又搞笑,但是它所需的联接会到导致当层次结构很深或很宽时,使用它的代价相对会比较高.层次结构越深,那么它将在最后采用更多的联接来组装具体的实体实例,层次结构越宽,那么它将采用更多的联接来通过实体超类进行查询.
联接继承数据模型:@Entity @Table(name="EMP") @Inheritance(strategy=InheritanceType.JOINED) @DiscriminatorColumn(name="EMP_TYPE",discriminatorType=DiscriminatorType.INTEGER) public abstract class Employee { } @Entity @Table(name="CONTRACT_EMP") @DIscriminatorValue("1") public class COntractEmployee extends Employee { } @MappedSuperclass public abstract class CompanyEmployee extends Employee { } @Entity @Table(name="FT_EMP") @DiscriminatorValue("2") public class FullTimeEmployee extends CompanyEmployee { } @Entity @Table(name="PT_EMP") @DiscriminatorValue("3") public class PartTimeEmployee extends CompanyEmployee { }
每个具体表一个表策略
此数据体系结构的方向与实体数据的非规范化相反,它把每个具体实体类及其所有继承的状态映射到一个单独的表.这回带来导致所有的共享状态在所有继承它的具体实体的表中重新定义的影响,此策略吧是提供程序必须支持的,但是因为预期在API的未来版本中将会需要它,所以扔包含了它.
使用此侧路额的负面影响在于它使得跨类层次结构的多态查询的代价比其他策略更高.问题是它必须在每个子类表上发出多个不同的查询,或者使用一个UNION操作对它们全部进行查询.@Entity @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS) public abstract class Employee { @Id private int id; @Temporal(TemporalType.DATE) @Column(name="S_DATE") private Date startDate; } @Entity @Table(name="CONTRACT_EMP") @AttributeOverrides({ @AttributeOverride(name="name",column=@Column(name="FULLNAME")), @AttributeOverride(name="startDate",column=@Column(name="SDATE")) }) public class ContractEmployee extends Employee { @Column(name="D_RATE") private int dailyRate; private int term; } @MappedSuperclass public abstract class CompanyEmployee extends Employee { private int vacation; @ManyToOne private Employee manager; } @Entity @Table(name="FT_EMP") public class FullTimeEmployee extends CompanyEmployee { private long salary; @Column(name="PENSION") private long pensionContribution; } @Entity @Table(name="PT_EMP") @AssociationOverride(name="manager",joinColumns=@JoinColumn(name="MGR")) public class PartTimeEmployee extends CompanyEmployee { @Column(name="H_RATE") private float hourlyRate; }
每个具体类一个表的数据模型: