1 Hibernate的关联映射

        关联关系就是指实例之间的互相访问关系,关联关系是面向对象分析、面向对象设计最重要的知识。关联关系大致有如下两个分类: 

        单向关系:只需单向访问关联端,包括单向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注解可以指定的属性如表所示:

@ManyToOne支持的属性

1.1.1 无连接表的N-1关联

        对于无连接表的N-1关联而言,程序只要在N的一端增加一列外键,让外键值记录该对象所属的实体即可,Hibernate可使用@JoinColumn来修饰代表关联实体的属性,@JoinColumn用于映射底层的外键列,@JoinColumn可指定的属性如图所示:

@JoinColumn支持的属性

        N端持久化类如下所示:

Person.java

        级联策略CascadeType.ALL表明对Person实体的所有持久化操作都会级联到它关联的Address实体。而程序无需从Address访问Person,所以Address无须增加Person属性,1端持久化类瑞所示:

Address.java

        先后创建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可指定的属性如图所示:

@JoinTable支持的属性

        N端持久化类如下所示:

Person.java

        @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注解可指定的属性如表所示:

@OneToOne支持的属性

1.2.1 基于外键的单向1-1关联

        对于基于外键的1-1关联,只要先使用@OneToOne注解修饰代表关联实体的属性,在使用@JoinColumn映射外键列即可。由于是1-1关联,因此应该为@JoinColumn增加unique=true属性。

        1端Person持久化类如下所示:

Person.java

            查看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注解可指定的属性如表所示:

@OneToMany支持的属性

1.3.1 无连接表的单向1-N关联

        无连接表的1-N单向关联需要在N的一端增加外键列来维护关联关系,因为是让1的一端来控制关联关系,所以需要在1的一端使用@JoinColumn来修饰Set集合属性、映射外键列。

        1端的持久化类如下所示:

Person.java

        此时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.java

        将person_address连接表的address_id添加唯一性约束,可以保证每个Address实例最多只能关联一个Person实例,但一个Person实例可以关联多个Address实例。使用连接表的1-N关联,两个实体对应的数据表都无须增加外键列,因此对应的数据表不存在主从关系,无论先持久化哪个实体,都不会引发性能问题。

1.4 单向N-N关联

        单向的N-N关联和1-N关联的持久化类代码完全相同,控制关系的一端需要增加一个Set类型的属性,被关联的持久化实例已结合形式存在。N-N关联需要使用@ManyToMany注解来修饰代表关联实体的集合属性,@ManyToMany可制定的属性如表所示:


@ManyToMany支持的属性

        N-N关联必须使用连接表,N-N关联与有连接表的1-N关联非常相似,区别就是N-N关联要去掉@JoinTable注解的inverseJoinColumns属性所制定的@JoinColumn中的unique=true属性。

        维护单向N-N关联关系的持久化类如下所示:

Person,java

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端持久化类如下所示:

Person.java

        N端持久化类如下所示:

Address.java

        使用双向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端持久化类如下所示:

Address.java

        因为采用了连接表,所以1端控制关联关系,并不会影响程序性能。如果程序希望Person实体也可以控制关联关系,那么Person类里就需要删除@OneToMany注解的mappedBy属性,并添加@JoinTable注解。

        可控制关联关系的1端持久化类如下所示:

Person.java

        观察Person类的结构我们可以发现,Person类和Address类的joinColumns和inverseJoinColumns两个属性是相反的,但是是相互对应的。

1.6 双向N-N关联

        双向N-N关联只能用连接表来建立两个实体之间的关联关系,两端添加Set集合属性,并且使用@ManyToMany修饰Set集合属性,两端都使用@JoinTable显示映射连接表。如果程序希望某一段放弃控制关联关系,可以在该端@ManyToMany注解中指定mappedBy属性。

        两端持久化类如下所示:

Person.java
Address.java

        观察上面的两个持久化类可以发现,两个@JoinTable指定的连接表的表名是相同的,joinColumns和inverseJoinColumns两个属性是相反的,但是是相互对应的。

1.7 双向1-1关联

        双向1-1关联的两端都需要使用@OneToOne注解进行映射。

1.7.1 基于外键的双向1-1关联

        基于外键的双向1-1关联,外键可以存放在任意一端。存放外键的一端需要增加unique=true属性来表示该实体实际上是1的一端。不维护外键的1端不应该控制关联关系,否则会导致生成额外的update语句,引起性能下降。

        不维护外键的1端如下所示:

Person.java

        维护外键的1端如下所示:

Address.java

        以上映射关系是可以互换的,即由Person负责控制关联关系,但是不要两端都使用相同的注解进行映射。

1.7.2 有连接表的双向1-1关联

        极少使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342