Pro JPA2 第四章(对象-关系映射)
把对象持久化到关系数据库的API的最大部分是对象-关系映射(Object-Relational Mapping,ORM)组件
- 4.1 持久化注解
持久化注解可以应用于三个不同的级别:类,方法和字段.无论如何进行何种级别的注解,注解必须放置在所注解项目的代码定义之前.
JPA注解设计为可读,易于指定以及有足够的灵活性以允许不同元数据的组合.大多数注解是同级指定而不是彼此嵌套的,这意味着多个注解可以注解在同一个类,字段或者属性,而不是嵌入在其他注解中进行注解.
映射注解可以归类为两种类型:逻辑注解(logical annotation)和物理注解(physical annotation).逻辑组的注解从对象模型视图(object modeling view)描述实体模型.它们与域模型仅仅绑定.物理注解与数据库中的具有数据模型相关.它们处理表,列,约束和数据库级的其他项目. 4.2 访问实体状态
4.2.1 字段访问
注解实体的字段将导提供程序使用字段访问来获取和设置该实体的状态.getter和setter方法可能存在或不存在,但如果存在,将会被忽略.所有字段必须声明为受保护(protected),包(package)或私有(private).不允许使用共有字段.@Entity public class Employee { @Id private int id; private String name; private long salary; // 省略getter,setter方法. }
4.2.2 属性访问
当使用属性访问模式时,将应用与JavaBeans相同的协定,而且持久化属性必须有getter和setter方法.属性的类型由getter方法的返回类型决定,同时必须与传递到setter方法的单个参数的类型相同.两种方法必须具有公共(public)或受保护(protected)的可见性.属性的映射注解必须放置在getter方法上.@Entity public class Employee { private int id; private String name; private long wage; @Id public int getId(){return id;} public void setId(int id){this.id=id;} public long getSalary(){return wage;} public void setSalary(long salary){this.wage = salary;} // 省略其他getter和setter方法 }
在上边的代码中,Employee类在getter方法getId()上游一个@Id注解,因此提供程序将使用属性访问以获取和设置该实体的状态.name和salary属性将凭借为它们而存在的getter和setter方法获得持久化,并将分别映射到NAME和SALARY列.
4.2.3 混合访问
在实体的同一个层次结构,甚至在相同的实体内,还可以结合字段访问和属性访问.例如,当一个实体的子类添加到现有的层次中,而该子类使用一个不同的访问类型的时候.在子类实体上添加一个指定了访问模式的@Access注解,将导致覆盖该实体子类的默认访问类型.
当您在读取或者写入数据到数据库的过程中,需要执行一个简单的数据转换时,@Access注解也是有用的.
一般,为了添加一个持久化字段或属性,并且将以不同于该实体的默认访问模式访问它时,需要三个基本步骤.
考虑一个Employee实体,它有一个默认的FIELD访问模式,但是数据库列存储区号作为电话号码的一部分,如果它不是本地号码,那么在实体的phoneNum字段中仅希望存储区号.可以添加一个持久化属性用于在读取和写入时进行相应的转换.
必须做的第一件事实显式地标记类的默认访问模式,通过@Access注解对它进行注解,并指示访问的类型.如果不这么做,那么字段和属性都将会被注解,那么他就是未定义的.@Entity @Access(AccessType.Field) public class Employee { //... }
下一步是通过@Access注解 注解其他的字段或属性,此时要指定与类级别相反的访问属性.这样做是为了有意识的区别于默认情况.
@Access(AccessType.PROPERTY) @Column(name="PHONE") protected String getPhoneNumberForDb(){ // ... }
最后一步是必需把要使其具有持久化的字段或属性标记为临时的(transient),从而默认的访问规则不会导致同样的状态被持久化两次.
@Transient private String phoneNum;
使用混合访问:
@Entity @Access(AccessType.FIELD) public class Employee { public static final String LOCAL_AREA_CODE = "613"; @Id private int id; @Transient private String phoneNum; @Access(AccessType.PROPERTY) @Column(name="PHONE") protected String getPhoneNumForDb(){ if (phoneNum.length() == 10){ return phoneNum; } else { return LOCAL_AREA_CODE + phoneNum; } } protected void setPhoneNumberForDb(String num) { if (num.startsWith(LOCAL_AREA_CODE)) { phoneNum = num.substring(3); } else { phoneNum = num; } } // .... 省略其他getter和setter方法 }
4.3 映射到表
重写默认的表名:@Entity @Table(name="Emp") public class Employee {...}
tips:默认名称米有指定是大写还是小写.(mysql如果不做设置的话是区分大小写的)
@Table注解不仅能够命名存储实体状态的表,而且还能命名数据库架构(schema)或目录(catalog)
@Entity @Table(name="EMp",schema="HR") public class Employee {...}
当持久化提供程序在数据库中访问表时,如果指定了架构名称,则它会将作为表名的前缀.例如:HR.EMP
有些数据库还支持目录(catalog)的概念,可以用@Table注解的catalog元素@Entity @Table(name="EMP",catalog="HR") public class Employee {...}
4.4 映射简单类型
将简单的Java类型映射为实体字段或属性中立即状态(immediate state)的一部分.可持久化类型的列表如下:- 基本的Java类型:byte,int,short,long,boolean,char,float,double.
- 基本的Java类型包装类:Byte,Integer,Short,Long,Boolean,Character,Float,Double.
- 字节和字符数据类型:byte[],Byte[],char[],Character[].
- 大数值类型: java.lang.BigInteger,java.math.BigDecimal.
- 字符串: java.lang.String.
- Java时间类型: java.util.Date,java.util.Calendar.
- JDBC时间类型: java.sql.Date,java.sql.Time,java.sql.Timestamp
- 枚举类型
- 序列化对象
如果所映射的数据库类型不完全与Java类型相同,那么提供程序会把JDBC返回的类型转换成正确的Java类型.如果不能正确转换,则有可能会抛出异常.
一个可选的@Basic注解可以放置在一个字段或者属性上,以显式地标记其正在持久化.该注解主要是为了文档化的目的,对于字段或者属性的持久化不是必需的.由于此注解,我们称简单类型的映射为基本映射. 4.4.1 列映射
重写列名:@Entity public class Employee { @Id @Column(name="EMP_ID") private int id; private String name; @Column(name="SAL") private long salary; @Column(name="COMM") private String comments; }
4.4.2 延迟提取
我们希望有些数据只有当需要的时候才去提取,可以通过在相应的@Basic注解中指定fetch元素,可以把基本映射的提取类型(fetch type) 配置为延迟或者即时加载.FetchType枚举类型定义了这个元素.它有EAGER和LAZY两种类型.所有的基本映射都默认为即时加载.
延迟加载字段:@Entity public class Employee { @Basic(fetch=FetchType.LAZY) @Column(name="COMM") private String comments; }
4.4.3 大型对象
使用@Lob注解可以将字段映射为CLOB或者BLOB类型.其中CLOB为字符型大型对象,BLOB为字节大型对象.@Entity public class Employee { @Id private int id; @Basic(fetch=FetchType.LAZY) @Lob @Column(name="PIC") private byte[] picture; }
4.4.4 枚举类型
public enum EmployeeType { FULL_TIME_EMPLOYEE, PART_TIME_EMPLOYEE, CONTRACT_EMPLOYEE }
使用序号映射枚举类型
@Entity public class Employee { @Id private int id; private EmployeeType type; }
这种情况下,每个Employee中的type都会被分配一个相应序号的EmployeeType.但是枚举类型的序号是不可改变的,如果当type发生改变后,数据库中并不会发生相应变化.如何解决这个问题?我们在数据库中存储枚举类型的字符串.我们可以通过@Enumerated注解,并指定一个STRING值来达到此目的.
@Entity public class Employee { @Id private int id; @Enumerated(EnumType.STRING) private EmployeeType type; }
4.4.5 时间类型
时间类型的实体字段可以通过@Temporal注解进行注解,并且通过指定JDBC类型为TemporalType枚举类型的值来实现这一点.TemporalType枚举类型有三个值:DATE,TIME,TIMESTAMP.@Entity public class Employee { @Id private int id; @Temporal(TemporalType.DATE) private Calendar dob; @Temporal(TemporalType.DATE) @Column(name="S_DATE") private Date startDate; }
4.4.6 瞬态
对于持久化实体的一部分但不打算具有持久化的特性.可以使用瞬态修饰符或者@Transient进行注解.@Entity public class Employee { @Id private int id; private String name; private long salary; // 或者@Transient transient private String translatedName; public String toString(){ if(this.translatedName == null){ translatedName = ResourceBundle.getBundle("EmpResources").getString("Employee"); } return translatedName + ":" + id + " " + name; } }
4.5 映射主键
- 4.5.1 重写主键列
应用到id映射的默认规则与基本映射基本相同,@Column注解可以用来重写id特性所映射的列名.
主键一般是可插入的,但不可为空且不可更新.当重写一个主键列时,不应重写nullable和updateable元素. - 4.5.2 主键类型
除了在指定映射到主键列时的特殊意义,一个id映射与基本映射几乎相同.其他主要区别是id映射通常限于以下类型:
- 基本的Java类型:byte,int,short,long,char.
- 基本的Java类型包装类:Byte,Integer,Short,Long,Character.
- 字符串: java.lang.String.
- 大型数值类型: java.math.BigInteger.
- 时间类型:java.util.Date,java.sql.Date.
浮点类型是允许的,同样Float和Double和java.math.BigDecimal也是允许的.
4.5.3 标识符生成.
有时候,应用程序不想受限于试图定义和确保域模型在某些方面的唯一性,因此,期望为它们自动上横撑标识符值.我们称之为id生成,并使用@GeneratedValue注解来指定它.
直到发生刷新之后或者事务已经完成时,应用程序才能访问该标识符.
我们通过strategy元素指定id生成策略.策略值是GenerationType枚举类型的AUTO,TABLE,SEQUENCE或者IDENTITY中的一个.自动id生成
为strategy元素设置为AUTO,提供程序就会创建一个标识符值,并且插入到每个获得持久化的实体的id字段.@Entity public class Employee { @Id @GeneratedValue(strategy = GenerationType.AUTO) private int id; }
不过使用AUTO策略有一个前提条件,提供程序选择自己的策略用于存储标识符,但是为了这么做它需要拥有某种持久性资源.例如,如果它选择基于表的策略,那么它需要创建一个表;如果它选择基于序列的策略,那么它需要创建一个序列.提供程序不能永远依赖于从服务器获取的数据库连接,来或得在数据库中创建表的权限.它需要再某种创建阶段或架构中生成,从而在AUTO策略工作之前能够创建资源.
在正式的应用环境中,最好不要使用AUTO策略,它实际上是一种用于开发或原型制作的生成策略.使用表的id生成
这种策略是最灵活和可移植的方案.
最简单使用TABLE生成策略:@Id @GeneratedValue(strategy=GenerationType.TABLE) private int id;
这种方式指示了生成策略但是没有置顶生成器,所以提供程序将假定一个自己选择的表.如果使用架构生成,那么将创建它,如果不是,那么由提供程序假定的默认表必须是已知的并且必须存在于数据库中.
另一种用于id的TABLE生成策略是明确指定用于id存储的表.我们可以使用@TableGenerator注解来定义它.然后利用@GeneratedValue注解中的名称来引用它.@TableGenerator(name="Emp_Gen") @Id @GeneratedValue(generator="Emp_Gen") private int id;
@TableGenerator 实际上还可以定义在任意特性或类上.不论在何处定义它,在整个持久性单元中它都将是可用的.一个好的做法是,如果只有一个类使用它,那么在id特性上本地定义它,如果它将用于多个类,那么在XML中定义它.
@TableGenerator注解的name元素将全局地命名生成器,然后就能够在@GeneratedValue中引用它.@TableGenerator(name="Emp_Gen", table="ID_GEN", pkCloumnName="GEN_NAME", valueColumnName="GEN_VAL")
其中”Emp_Gen”存储标识符的实际表.table元素仅仅指示表的名称.pkCloumnName元素是在表中唯一标识生成器的主键列的名称,而valueColumnName元素是存储实际生成的id序列值的列的名称.
为了避免每次请求标识符时都更新行,将使用分配大小(allocation size),这将导致提供程序预先分配一批标识符,然后在请求时从内存中生成标识符,直到这批标识符用完.默认情况下,分配大小设置为50.可通过使用allocationSize元素重写此值.@TableGenerator(name="Emp_Gen", table="ID_GEN", pkCloumnName="GEN_NAME", valueColumnName="GEN_VAL", initialValue=10000, allocationSize=100 ) @Id @GeneratedValue(generator="Address_Gen") private int id;
如果同时定义了”Emp_Gen”和”Address_Gen”生成器,那么ID_GEN如图所示:
如果没有使用自动模式架构功能(13章),那么表必须已经存在或者通过一些其他方法在数据库中创建它.使用数据库序列的id生成
@Id @GeneratedValue(strategy=GenerationType.SEQUENCE) private int id;
此时,提供程序将使用它自己选择的一个默认序列对象.
如果定义了多个序列生成器但是没有命名,那么不指定它们是否使用相同或不同的默认序列.安全的做法是 定义一个命名的序列生成器,并在@GeneratedValue注解中引用它.@SequenceGenerator(name="Emp_Gen",sequenceName="Emp_Seq") @Id @generatedValue(generator="Emp_Gen") private int getId;
初始值和分配大小也可以用于序列生成器.
使用数据标识符的id生成.
@Id @GeneratedValue(strategy=GenerationType.IDENTITY) private int id;
当使用IDENTITY时,是插入操作导致标识符的生成,因此,标识符在实体插入到数据之前不可能可用.同事,因为实体插入往往延迟到提交时间,所以标识符只有在事务提交后才可用.
- 4.5.1 重写主键列
4.6 关系
4.6.1 关系概述
- 角色
任何实体可能会在任何给定的魔性中扮演一些不同的角色. 方向性
单向关系:
双向关系:可用使用关系的方向性,以版主描述和说明一个模型,但是当在具体的术语中讨论它时,把每一个双向关系当做一对单向关系会比较合理.每个这样的关系都有一个实体是源或引用的角色,而另一边是目标或被引用的角色:
基数
指示关系关联中是一对多的关系还是多对多的关系.例如,一个员工在一个部门工作,双方的基数都是1.一个部门中有多个员工,员工的基数是many,部门的基数是1.序号性
通过确定一个角色双方存在,可以进一步地指定它,我们称之为序号性(ordinality),其有助于表明当创建源实体时,是否需要指定目标实体,因为序号性实际上只是一个布尔值,所以把他称为关系的可选性(optionality).
- 角色
- 4.6.2 映射概述
- 多对一(Many-to-one)
- 一对一(One-to-one)
- 一对多(One-to-many)
- 多对多(Many-to-many)
这些名称也是注解的名称,用来指示在特性上映射的关系类型.它们是逻辑关系注解的基础.
像基本映射一样,关系映射可以应用于实体的字段或属性.
4.6.3 单值关联(从一个实体实例关联到另一个实体实例,其中目标的基数是”一”)
多对一映射
通过使用@ManyToOne注解来实现.@Entity public class Employee { @ManyToOne private Department department; }
Employee中的department字段是被注解的源特性.
从 Employee到Department的多对一关系使用联结列
数据库中的外键在JPA中称它们为联结列(join column),
而@JoinColumn注解是主要用于配置这些类型的列的注解.
几乎在每个关系中,与源方和目标方无关,双方中的一个将在其表中拥有联结列,拥有联结列的一方称为关系的所有者,另一方称之为非关系所有者.
@JoinColumn注解总是在关系的所有方定义,如果它们不存在,那么值的默认值将从所有方特性的角度来考虑.
多对一关系总是在关系的所有方只上.为了指定联结列的名称,可以使用name元素.
如果@JoinColumn注解没有与多对一映射同时出现,那么将会假定一个默认的列名称.默认的名称是结合源和目标实体而成.@Entity public class Employee { @Id private int id; @ManyToOne @JoinCloumn(name="DEPT_ID") private Department department; }
一对一映射
使用@OneToOne代替@ManyToOne@Entity public class Employee { @Id private int id; @OneToOne @JoinColumn(name="PSPACE_ID") private ParkingSpace parkingSpace; }
就像多对一映射一样,一对一映射有一个在数据中的联结列,并且当默认名称不适用时,需要再@JoinColumn注解中重写列的名称.
双向一对一映射
一对一的目标实体经常会有指回源实体的关系.
您已经知道包含联结列的实体决定了作为关系所有者的实体.在双向一对一的关系中,两个映射均是一对一映射,两方均可以是所有者.实体的一对一关系可以通过@OneToOne注解来实现,作为注解的一部分,必须添加一个mappedBy元素以只是所有方是Employee,而不是ParkingSpace.因为ParkingSpace是关系的反方,所以它不必提供联结列信息.@Entity public class ParkingSpace { @Id private int id; @OneToOne(mappedBy="parkingSpace") private Employee employee; }
双向一对一关联的两条规则如下:
- @JoinColumn注解放置在包含联结列的表的实体的映射只上.
- mappedBy元素应该在没有定义联结列的实体的@OneToOne注解中指定.
如果在双向关联的两方俊又mappedBy,那么它是不合法的,同样,若在两方均没有mappedBy,则也是不正确的.因为,如果它在关系的双方中均不存在,那么提供程序将把没放当做一个独立的单向关系.
4.6.4 集合值关联
一对多映射
当一个实体与其他实体的集合相关联时,最常采用一对多映射的形式.
当一个关系是双向时,实际上有两个映射,每个方向有一个.双向多对一关系意味着一个从目标指回源的一对多映射.
当一个源实体有任意数量的目标实体存储在它的集合中时,没有可扩展的方式可以用于在数据库表中存储这些它所映射到的引用.如何在单行中存储任意数量的外键?相反,必须让集合中的实体表具有指回到源实体表的外键.这就是为什么一对多关联几乎总是双向的,而不仅仅是所有方.@Entity public class Department { @Id private int id; @OneToMany(mappedBy="department") private Collection<Employee> employees; }
需要注意的地方: 使用Collection来存储Employee实体.这样将提供严格的类型匹配,保证数据的安全性.
如果不使用泛型,而只是简单的使用Collection类型,那么需要使用targetEntity元素来指定实体类型,以达到和使用泛型相同的目的.@Entity public class Department { @Id private int id; @OneToMany(targetEntity=Employee.class,mappedBy="department") }
- 多对一方是所有方,所以在那一方定义联结列.
- 一对多是反方,必须使用mappedBy元素.
如果在@OneToMany注解中没有指定mappedBy元素,那么它将会被视为单向的一对多关系,将其定义为使用链接表.
多对多映射
关系中每一方的每个实体将与一个集合值相关联,其中包含了目标类型的实体.
利用@ManyToMany对实体的集合特性进行注解,从而在源和目标实体只上表示多对多映射.@Entity public clas Employee { @Id private int id; @ManyToMany private Collection<Project> projects; } @Entity public class Project { @Id private int id; @ManyToMany(mappedBy="projects") private Collection<Employee> employees; }
多对多关系和一对多关系之间有一些重要的区别:
- 数学的必然性,当多对多关系是双向时,关系双方均是多对多关系.
- 双方均没有联结列.实现多对多关系的唯一途径是利用一个单独的链接表(join table).在任何实体表中均没有联结列的后果是没有办法确定哪一方是关系的所有者.
使用联接表
因为多对多关系中双方的多重性(multiplicity)均是复数,所以没有哪个实体表可以在单个实体行中存储无限多的外键值集合.必须使用第三个表来关联两个实体类型.我们称这样的表为联接表(join table).
为了实现联接表,需要再Employee类中添加一些额外的元数据.@Entity public class Employee { @Id private int id; @ManyToMany @JoinTable(name="EMP_PROJ", joinColumns=@JoinColumn(name="EMP_ID"), inverseJoinColumns=@JoinColumn(name="PROJ_ID")) private Collection<Project> projects; }
@JoinTable注解用来配置关系的联接表.通过所有方和反方对联接表中的两个联结列进行区分.在JoinColumns元素中描述所有方的联结列,而在inverseJoinColumns元素中指定反方的联结列.
单向集合映射
当一个实体到目标实体存在一对多映射,但是@OneToMany注解不包含mappedBy元素时,就认为它存在与目标实体的单向关系.这意味着目标实体不具有指回源实体的多对一映射.
使用联接表以关联Phone实体和Employee实体.
同样,当多对多关系中的一方没有映射到另一方时,它就是一个单向的关系.仍然必须使用联接表.@Entity public class Employee { @Id private int id; private String name; @OneToMany @JoinTable(name="EMP_PHONE", joinColumns=@JoinColumn(name="EMP_ID"), inverseJoinColumn=@JoinColumn(name="PHONE_ID")) private Collection<Phone> phones; }
延迟关系
使用延迟加载@Entity public class Employee { @Id private int id; @OneToOne(fetch=FetchType.LAZY) private ParkingSPace parkingSpace; }
tips:指定为延迟加载的关系,当使用getter方法访问对象时,可能会导致不加载相关对象.对象可能是一个代理.但是可能出现session已经被关闭等等的情况.
4.7嵌入对象
嵌入对象(embedded object)依赖于一个实体确定其标识.它没有自己的标识,而仅仅作为实体状态的一部分,它被单独提取出来,存储在一个单独的Java对象中,并且该对象附在实体之上.在数据库中,嵌入对象的状态与实体的其他状态存储在数据库的行中,在Java实体中的状态与在其嵌入对象中的状态之间没有区别.
回顾第一章对象关系的阻抗失谐,说说为什么要用嵌入对象.数据库记录包含一个以上的逻辑类型,从而使得即使采用不同的物理表示,也可以显式化应用程序中对象模型的关系.与简单的实体特性集合相比,嵌入对象几乎总是一种更自然的域概念的表示方法.此外,一旦确定了一组实体状态用于构成一个嵌入对象,就可以与其他具有相同内部表示的实体,共享相同的嵌入对象类型.
在下图中,Address对象除了拥有它的Employee实体意外,Address实例不可以被其他任何对象所共享.
采用这种方式,不进地址信息可以整齐的封装在一个对象中,而且如果另一个实体也拥有地址信息,那么,它也可以有一个特性志向它自身的嵌入Address对象.
通过在类定义中添加@Embeddable注解来标记嵌入类型.此注解有助于区分该类和其他常规的Java类型.一旦某个类指定为可嵌入的,它的字段和属性就将作为实体的一部分变得可持久化.@Embeddable @Access(AccessType.FIELD) public class Address { private String street; private String city; private String state; @Column(name="ZIP_CODE") private String zip; }
为了在实体中使用此类,实体只需要用友一个可嵌入类型的特性.该特性采用@Embedded注解来标识.
@Entity public class Employee { @Id private int id; private long salary; @Embedded private Address addresses; }
两个实体共享的地址
假如嵌入类型Address的列映射应用到其包含入实体的列,那么如果两个实体表对于相同字段有不同的列名称,那么应该如何共享.对于每个嵌入对象的特性,如果想要再实体中重写它们,那么可以使用@AttributeOverride注解.注解实体中的嵌入字段或属性,并在name元素中指定正在重写的嵌入对象字段或属性.column指定在实体表中所映射的列.@Entity public class Employee { @Id private int id; private String name; private long salary; @Embedded @AttributeOverrides({ @AttributeOverride(name="state",column=@Column(name="PROVINCE")), @AttributeOverride(name="zip",column=@Column(name="POSTAL_CODE")) }) private Address address; } @Entity public class Company { @Id private String name; @Embedded private Address address; }