关联关系就是指实例之间的互相访问关系,关联关系是面向对象分析、面向对象设计最重要的知识。关联关系大致有如下两个分类:
单向关系:只需单向访问关联端,包括单向1→1、单向1→N、单向N→1、单向N→N四种关系。
双向关系:关联的两端可以互相访问,包括双向1-1、双向1-N、双向N-N三种关系。双向关系里没有N-1,因为双向关系1-N和N-1是完全相同的。
在接下来的学习中,我们要构建一个Person与Address的数据模型,通过这个数据模型来讲解Hibernate的关联映射关系。
1.1 单向N-1关联
单向的N-1关联只需要从N的一端访问1的一端。为了让两个持久化类支持这种关联映射,程序应该在N的一端的持久化类中增加一个属性,该属性引用1的一端的关联实体。对于N-1关联(不管是单向关联,还是双向关联)都需要在N的一端使用@ManyToOne修饰代表关联实体的属性。@ManyToOne注解可以指定的属性如表所示:
1.1.1 无连接表的N-1关联
对于无连接表的N-1关联而言,程序只要在N的一端增加一列外键,让外键值记录该对象所属的实体即可,Hibernate可使用@JoinColumn来修饰代表关联实体的属性,@JoinColumn用于映射底层的外键列,@JoinColumn可指定的属性如图所示:
N端持久化类如下所示:
级联策略CascadeType.ALL表明对Person实体的所有持久化操作都会级联到它关联的Address实体。而程序无需从Address访问Person,所以Address无须增加Person属性,1端持久化类瑞所示:
先后创建Address类和Person类的实例,将Address类的实例注入到Person类实例的address属性中,此时直接保存Person实例,因为持久化类中设置了Cascade.ALL的级联属性,所以将会触发两次操作,Hibernate首先保存Address实例,然后再保存Person实例,此时Hibernate插入的Person记录的address_id外键的值就是刚插入的Address记录的主键值。
如果未设置级联属性,则保存Person实例时,系统会抛出TransientPropertyValueException异常,因为Person实例参照的Address实例未曾保存;如果未设置级联属性,但是Person类的address属性映射的外键列允许为null(上面程序设置nullable=false,即不允许为null),则Hibernate会先插入address_id外键列为null的Person记录,Person实例关联的Address实例被保存之后,Hibernate会执行一条update语句来为Person记录填充address_id外键值,从而建立Person实体与Address实体之间的关联关系。
综上所述,在基于外键约束的关联关系中,要么总是先持久化主表记录对应的实体,要么设置级联操作,否则会是Hibernate抛出异常或多执行update语句影响程序性能。
1.1.2 有连接表的N-1关联
如果需要使用连接表来映射单向N-1关联,程序需要显式使用@JoinTable注解来映射连接表,@JoinTable可指定的属性如图所示:
N端持久化类如下所示:
@JoinTable注解强制指定使用连接表来维护单向N-1关联,连接表名为person_address。第一个@JoinColumn注解增加了unique=true属性,保证person_address表的person_id列不能出现重复值,即一个Person实例只能关联一个Address实例。使用连接表的N-1关联,两个实体对应的数据表都无须增加外键列,因此对应的数据表不存在主从关系,无论先持久化哪个实例,都不会引发性能问题。
1.2 单向1-1关联
单向1-1关联的持久化类,与单向N-1关联的持久化类相同,都是在N的一端或1的一端增加代表关联实体的属性。对于1-1关联(不管是单向关联,还是双向关联),都需要使用@OneToOne修饰代表关联实体的属性,@OneToOne注解可指定的属性如表所示:
1.2.1 基于外键的单向1-1关联
对于基于外键的1-1关联,只要先使用@OneToOne注解修饰代表关联实体的属性,在使用@JoinColumn映射外键列即可。由于是1-1关联,因此应该为@JoinColumn增加unique=true属性。
1端Person持久化类如下所示:
查看Person类的代码可以发现,单向1-1关联就是在单向N-1关联的基础上,为Person表的address_id增加了唯一性约束,即保证了一个Person实体只能对应唯一一个Address实体。
1.2.2 有连接表的单向1-1关联
极少使用。
1.3 单向1-N关联
单向1-N关联的持久化类里需要使用集合属性,因为1的一端需要访问N的一端,而N的一端将以集合(Set)形式表现。对于单向的1-N关联关系,只需要在1的一端增加Set类型的成员变量,该成员变量记录当前实体所有的关联实体。为了映射1-N关联,Hibernate需要使用@OneToMany注解,@OneToMany注解可指定的属性如表所示:
1.3.1 无连接表的单向1-N关联
无连接表的1-N单向关联需要在N的一端增加外键列来维护关联关系,因为是让1的一端来控制关联关系,所以需要在1的一端使用@JoinColumn来修饰Set集合属性、映射外键列。
1端的持久化类如下所示:
此时addresses成员变量上添加的@JoinColumn映射了外键列,但是此处的外键列并不是增加到当前实体(Person)对应的数据表中,而是增加到关联实体(Address)对应的数据表中。因为Address实例不需要维护与Person实例的关联关系,因此Address类不需要修改。
首先创建Address类的实例,再创建Person类的实例,先持久化Address实例,此时Address实例对应的记录中的person_id字段为null,再将Address实例注入到Person实例的addresses集合中,持久化Person实例,Hibernate会先向数据库中插入Person实例的数据,然后将Address记录中person_id字段的值修改为Person记录的主键值。该过程首先持久化了Address实例,然后再持久化Person实例,最后造成的结果就是多执行了一条update语句;如果为@OneToMany注解添加Cascade.ALL属性,可以不用先持久化Address实例,而是将Address实例注入到Person实例的addresses属性之后,直接持久化Person实例,但是依旧会像未设置级联时一样,执行三次数据库操作语句;如果未设置级联,又没有先持久化Address实例,那么持久化包含了瞬态Address实例的Person实例时,Hibernate就会报出TransientObjectException异常。
造成上述问题的根本原因,是因为从Person到Address的关联没有被当做Address实例状态的一部分,所以Address实例并不知道它所关联的Person实体,因而Hibernate无法在持久化Address实例时为person_id外键列指定值。解决该问题的方式,就是将这个关联关系添加到Address的映射中,这种方式也就是将单向1-N关联变成了双向1-N关联。因此,应该尽量少用单向1-N关联,改用双向1-N关联。
1.3.2 有连接表的单向1-N关联
对于有连接表的1-N关联,同样需要使用@OneToMany修饰代表关联实体的集合属性,此外,程序应该使用@JoinTable显示指定连接表。
1端持久化类如下所示:
将person_address连接表的address_id添加唯一性约束,可以保证每个Address实例最多只能关联一个Person实例,但一个Person实例可以关联多个Address实例。使用连接表的1-N关联,两个实体对应的数据表都无须增加外键列,因此对应的数据表不存在主从关系,无论先持久化哪个实体,都不会引发性能问题。
1.4 单向N-N关联
单向的N-N关联和1-N关联的持久化类代码完全相同,控制关系的一端需要增加一个Set类型的属性,被关联的持久化实例已结合形式存在。N-N关联需要使用@ManyToMany注解来修饰代表关联实体的集合属性,@ManyToMany可制定的属性如表所示:
N-N关联必须使用连接表,N-N关联与有连接表的1-N关联非常相似,区别就是N-N关联要去掉@JoinTable注解的inverseJoinColumns属性所制定的@JoinColumn中的unique=true属性。
维护单向N-N关联关系的持久化类如下所示:
1.5 双向1-N关联
双向的1-N关联与双向的N-1关联是完全相同的两种情形,两端都需要增加对关联属性的访问,N的一端增加引用到关联实体的属性,1的一端增加集合属性,集合元素为关联实体。
1.5.1 无连接表的双向1-N关联
无连接表的双向1-N关联,N的一端需要增加@ManyToOne注解来修饰代表关联实体的属性,并且使用@JoinColumn来映射外键列;而1的一端则需要使用@OneToMany注解来修饰代表关联实体的属性,由于不应该在1端控制关联关系,所以应该在使用@OneToMany注解时指定mappedBy属性。一旦为@OneToMany、@ManyToMany指定了mappedBy属性,则表明当前实体不能控制关联关系。Hibernate不允许那些放弃控制关联关系的实体,使用@JoinColumn或@JoinTable注解修饰代表关联实体的属性。
1端持久化类如下所示:
N端持久化类如下所示:
使用双向1-N关联关系时,需要注意一下几点:
(1)最好先持久化Person实例,因为程序希望持久化Address实例时,能够为Address记录设置外键值,所以Address记录参照的主表记录就必须要先存在于数据库中,即Person实例必须先与Address实例持久化。
(2)先将Person实例注入到Address实例中,再持久化Address实例。如果顺序反调,Hibernate就无法在持久化Address实例时设置记录的外键值,等到设置好关联关系后,Hibernate就需要再执行一条update语句来更新Address记录的外键值。
(3)不要通过Person对象设置关联关系。因为已经使用了mappedBy属性标记Person类不能控制关联关系的。
1.5.2 有连接表的双向1-N关联
有连接表的1-N关联关系,1的一端与无连接表1-N关联中的相同,1的一端依然不控制关联关系;N的一端使用@JoinTable注解显示指定连接表即可。
N端持久化类如下所示:
因为采用了连接表,所以1端控制关联关系,并不会影响程序性能。如果程序希望Person实体也可以控制关联关系,那么Person类里就需要删除@OneToMany注解的mappedBy属性,并添加@JoinTable注解。
可控制关联关系的1端持久化类如下所示:
观察Person类的结构我们可以发现,Person类和Address类的joinColumns和inverseJoinColumns两个属性是相反的,但是是相互对应的。
1.6 双向N-N关联
双向N-N关联只能用连接表来建立两个实体之间的关联关系,两端添加Set集合属性,并且使用@ManyToMany修饰Set集合属性,两端都使用@JoinTable显示映射连接表。如果程序希望某一段放弃控制关联关系,可以在该端@ManyToMany注解中指定mappedBy属性。
两端持久化类如下所示:
观察上面的两个持久化类可以发现,两个@JoinTable指定的连接表的表名是相同的,joinColumns和inverseJoinColumns两个属性是相反的,但是是相互对应的。
1.7 双向1-1关联
双向1-1关联的两端都需要使用@OneToOne注解进行映射。
1.7.1 基于外键的双向1-1关联
基于外键的双向1-1关联,外键可以存放在任意一端。存放外键的一端需要增加unique=true属性来表示该实体实际上是1的一端。不维护外键的1端不应该控制关联关系,否则会导致生成额外的update语句,引起性能下降。
不维护外键的1端如下所示:
维护外键的1端如下所示:
以上映射关系是可以互换的,即由Person负责控制关联关系,但是不要两端都使用相同的注解进行映射。
1.7.2 有连接表的双向1-1关联
极少使用。