码迷,mamicode.com
首页 > Web开发 > 详细

Hibernate学习笔记之关联关系

时间:2017-04-16 19:36:44      阅读:310      评论:0      收藏:0      [点我收藏+]

标签:注释   ble   持久化类   也会   type   alt   details   现象   专业   

??关联关系是面向对象分析、面向对象设计最重要的知识,Hibernate完全可以理解这种关联关系,如果映射得当,Hibernate的关联映射将可以大大简化持久层数据的访问。关联关系大致有如下两类:

  • 单向关系:只需单向访问关联端。
  • 双向关系:关联的两端可以互相访问

注:双向关系没有N—1,因为双向关系1—N和N—1是完全相同的。

注意:无论单向关系,还是双向关系,是针对面向对象中的类的调用,与底层数据库关系有所差异,不要搞混了。

专业名词通俗解释:

  • 持久化操作:将数据CRUD(增加(Create)、读取查询(Retrieve)、更新(Update)和删除(Delete))到数据库的过程,就是持久化操作。

@ManyToOne注解属性(全非必须):

  • cascade:指定Hibernate对关联实体采用怎样的级联策略:
    • CascadeType.ALL:指定Hibernate将所有的持久化操作都级联到关联实体
    • CascadeType.MERGE:指定Hibernate将merge操作级联到关联实体(更新)
    • CascadeType.PERSIST:指定Hibernate将persist操作级联到关联实体(保存)
    • CascadeType.REFRESH:指定Hibernate将refresh操作级联到关联实体(将数据库的数据更新到本地的实体中,而不是本地实体更新数据到数据库中)
    • CascadeType.REMOVE:指定Hibernate将remove操作级联到关联实体(删除)
  • fetch:指定关联实体时的抓取策略
    • FetchType.EAGER:抓取实体时,立即抓取关联实体。(默认值)
    • FetchType.LAZY:抓取实体时,延迟抓取关联实体。等到真正用到关联实体时才去抓取
  • optional:该属性指定关联关系是否可选
  • targetEntity:该属性指定关联实体的类名。在默认情况下,Hibernate将通过反射来判断关联实体的类名

@OneToOne注解属性(全非必须):

  • cascade:指定Hibernate对关联实体采用怎样的级联策略:
    • CascadeType.ALL:指定Hibernate将所有的持久化操作都级联到关联实体
    • CascadeType.MERGE:指定Hibernate将merge操作级联到关联实体(更新)
    • CascadeType.PERSIST:指定Hibernate将persist操作级联到关联实体(保存)
    • CascadeType.REFRESH:指定Hibernate将refresh操作级联到关联实体(将数据库的数据更新到本地的实体中,而不是本地实体更新数据到数据库中)
    • CascadeType.REMOVE:指定Hibernate将remove操作级联到关联实体(删除)
  • fetch:指定关联实体时的抓取策略
    • FetchType.EAGER:抓取实体时,立即抓取关联实体。(默认值)
    • FetchType.LAZY:抓取实体时,延迟抓取关联实体。等到真正用到关联实体时才去抓取
  • optional:该属性指定关联关系是否可选
  • targetEntity:该属性指定关联实体的类名。在默认情况下,Hibernate将通过反射来判断关联实体的类名
  • mappedBy:该属性合法的属性值为关联实体的属性名,该属性指定的关联实体中哪个属性可引用到当前实体
  • orphanRemoval:该属性设置是否删除“孤儿”实体。如果某个实体所关联的父实体不存在(即该实体对应记录的外键为null),该实体就是所谓的“孤儿”实体

@OneToMany注解属性(全非必须):(没有optional属性)

  • cascade:指定Hibernate对关联实体采用怎样的级联策略:
    • CascadeType.ALL:指定Hibernate将所有的持久化操作都级联到关联实体
    • CascadeType.MERGE:指定Hibernate将merge操作级联到关联实体(更新)
    • CascadeType.PERSIST:指定Hibernate将persist操作级联到关联实体(保存)
    • CascadeType.REFRESH:指定Hibernate将refresh操作级联到关联实体(将数据库的数据更新到本地的实体中,而不是本地实体更新数据到数据库中)
    • CascadeType.REMOVE:指定Hibernate将remove操作级联到关联实体(删除)
  • fetch:指定关联实体时的抓取策略
    • FetchType.EAGER:抓取实体时,立即抓取关联实体。(默认值)
    • FetchType.LAZY:抓取实体时,延迟抓取关联实体。等到真正用到关联实体时才去抓取
  • targetEntity:该属性指定关联实体的类名。在默认情况下,Hibernate将通过反射来判断关联实体的类名

  • mappedBy:该属性合法的属性值为关联实体的属性名,该属性指定的关联实体中哪个属性可引用到当前实体
  • orphanRemoval:该属性设置是否删除“孤儿”实体。如果某个实体所关联的父实体不存在(即该实体对应记录的外键为null),该实体就是所谓的“孤儿”实体

@ManyToMany注解属性(全非必须):

  • cascade:指定Hibernate对关联实体采用怎样的级联策略:
    • CascadeType.ALL:指定Hibernate将所有的持久化操作都级联到关联实体
    • CascadeType.MERGE:指定Hibernate将merge操作级联到关联实体(更新)
    • CascadeType.PERSIST:指定Hibernate将persist操作级联到关联实体(保存)
    • CascadeType.REFRESH:指定Hibernate将refresh操作级联到关联实体(将数据库的数据更新到本地的实体中,而不是本地实体更新数据到数据库中)
    • CascadeType.REMOVE:指定Hibernate将remove操作级联到关联实体(删除)
  • fetch:指定关联实体时的抓取策略
    • FetchType.EAGER:抓取实体时,立即抓取关联实体。(默认值)
    • FetchType.LAZY:抓取实体时,延迟抓取关联实体。等到真正用到关联实体时才去抓取
  • optional:该属性指定关联关系是否可选
  • targetEntity:该属性指定关联实体的类名。在默认情况下,Hibernate将通过反射来判断关联实体的类名

@JoinColumn注解专门用于定义外键列,包含属性(全非必需)如下:

  • columnDefinition:指定Hibernate使用该属性值指定的SQL片段来创建外键列
  • name:指定该外键列的列名
  • insertable:指定该列是否包含在Hibernate生成的insert语句的列列表中。默认值:true
  • updateble:指定该列是否包含在Hibernate生成的update语句的列列表中。默认值:true
  • nullable:指定该列是否允许为null,该属性的默认值:true
  • table:指定该列所在数据表的表名
  • unique:指定是否为该列增加唯一约束
  • referencedColumnName:指定该列所参照的主键列的列名

@Cascade注解用于指定级联操作策略(与上面的Cascade属性功能一样,只不过是Hibernate中的特有注解,其值也一样,只不过导入的包不一样,这一点需要注意)

@JoinTable注解用于映射底层连接表的信息,包含属性(全非必需)如下:

  • name:指定连接表的表名
  • catalog:设置该连接表放入指定的catalog中。如果灭有指定该属性,连接表将放入默认的catalog中
  • schema:设置该连接表放入指定的schema中。如果灭有指定该属性,连接表将放入默认的schema中
  • targetEntity:该属性指定关联实体的类名。在默认情况下,Hibernate将通过反射来判断关联实体的类名
  • indexes:该属性值为@Index注解数组,用于为连接表定义多个索引
  • JoinColumns:该属性可接受多个@JoinColumn,用于配置连接表中外键列的列信息,这些外键列参照当前实体对应表的主键列(也就是定义关联表关联当前对象所对应的表)
  • inverseJoinColumns:该属性值可接受多个@JoinColumn,用于配置连接表中外键列的列信息,这些外键列参照当前实体的关联实体对应表的主键列
  • uniqueConstraints:该属性用于为连接表增加唯一约束


1. 单向N—1关联

??对于N-1关联,需要在N的一端使用@ManyToOne修饰代表关联实体的属性。@ManyToOne注解位于Java EE API的 javax.persistence包中(Java Persistence is the API for the management for persistence and object/relational mapping)

1.1 无关联表的N-1关联

??对于无连接表的N-1关联而言,程序只要在N的一端增加一列外键,让外键值记录该对象所属的实体即可。

??Hibernate可使用@JoinColumn来修饰代表关联实体的属性,@JoinColumn用于映射底层的外键列。

??直接使用@JoinColumn注解来映射N-1关联时,Hibernate将无须使用连接表,直接使用外键关联策略策略来处理这种关联映射。
注意:当我们向关联表中插入数据,如果指定了级联操作,则系统先自动级联插入主表记录,再插入从表记录;否则,系统抛出TransientObjectException异常:object references an unsaved transient instance,因为主表记录不曾插入,所以参照该记录的从表记录无法插入。

??在所有既有的基于外键的约束的关联中,都必须牢记:要么总是先持久化主表记录对应的实体,要么设置级联操作;否则当Hibernate试图插入从表记录时,如果发现该从表记录参照的主表记录不存在,那一定会抛出异常。

示例如下(片段):

// 定义该Person实体关联的Address实体
@ManyToOne(targetEntity=Address.class)
// 映射外键列,指定外键列的列名为address_id、不允许为空
@JoinColumn(name="address_id" , nullable=false)
@Cascade(CascadeType.ALL)
private Address address;
详细代码可参考:codes\06\6.1\unidirectional\N-1nojointable

??上面代码生成的架构图如下:

技术分享

??这样该类就单向地持有一个Address,上面程序还额外使用了Hibernate本身提供的@Cascade注解,该注解用于指定级联操作策略。

??上面的实例中底层生成的数据表person_inf表会保存一个外键设置,保存该外键的列就是person_inf表的address_id列。

注意:使用@Cascade需要引入的包是org.hibernate.annotations.Cascade.*;包;而使用@ManyToOne的Cascade属性需要引入的是javax.persistence.*;包。

1.2 有连接表的N-1关联

??对于绝大部分单向N-1关联而言,使用基于外键的关联映射已经足够了。但由于底层数据库建模时也可以使用连接表来建立这种关联关系,因此Hibernate中可以显示使用@JoinTable注解来映射连接表。

??@JoinTable注解专门用于映射底层连击表的信息,示例如下(片段):

@Entity
@Table(name = "person_inf")
public class Person {
	// 标识属性
	@Id
	@Column(name = "person_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private String name;
	private int age;
	// 定义该Person实体关联的Address实体
	@ManyToOne(targetEntity = Address.class)
	// 显式使用@JoinTable映射连接表
	@JoinTable(name = "person_address", // 指定连接表的表名为person_address
			// 指定连接表中person_id外键列,参照到当前实体对应表的主键列
			joinColumns = @JoinColumn(name = "person_id_center", referencedColumnName = "person_id",unique=true),
			// 指定连接表中address_id外键列,参照到当前实体的关联实体对应表的主键列
			inverseJoinColumns = @JoinColumn(name = "address_id_center", referencedColumnName = "address_id"))
	private Address address;
	...
}
代码详见:codes\06\6.1\unidirectional\N-1jointable
??上面代码生成的中间表的架构图如下:

技术分享

??从架构图可以看出生成的中间表中person_id_center字段为主键,上面的关系可以这样解释:一个人只允许有一个地址,而一个地址可以对应多个人,其中N是person,1是address。

注意:既然是主键了,那么肯定是不能重复的,那么unique属性应该没有什么作用,我尝试去掉后,同样生成完全一样,正如猜想结果一样,但为什么还要加上呢?其实也是有原因的,因为在联合主键中,单独的一个主键是无法保证唯一性的,在这里当然可以。

??对于使用连接表的N-1关联关系而言,由于两个实体对应的数据表都无需增加外键列,因此两个实体对应的数据表不存在主、从关系,程序完全可以想先持久化哪个实体,就持久化哪个实体。无论先持久化哪个实体,程序都不会引发性能问题。

2. 单向1—1关联

??对于单向的1—1关联关系,需要在持久化类里增加代表关联实体的成员变量,并为该成员变量增加setter和getter方法。从持久化类的代码上看,单向1—1与单向N—1没有丝毫区别。因为它们前者都是直接访问关联实体,只需增加代表关联实体的属性即可。

2.1 基于外键的单向1—1关联

??对于基于外键1—1关联而言,只要先使用@OneToOne注解修饰代表关联实体的属性,再使用@JoinColumn映射外键即可。

注:由于是1—1关联,因此应该为@JoinColumn增加unique=true。如果你不添加这个属性,就会变成单向的N—1关系。

示例如下:

// 定义该Person实体关联的Address实体
@OneToOne(targetEntity = Address.class)
// 映射名为address_id的外键列,参照关联实体对应表的addres_id主键列
@JoinColumn(name = "address_id_fk", referencedColumnName = "address_id", unique = true)
private Address address;
代码详见:codes\06\6.1\unidirectional\1-1FK
??上面代码的架构图如下:

技术分享

2.2 有连接表的单向1—1关联

??这种情况很少见,但Hibernate同样支持,示例代码如下:

// 定义该Person实体关联的Address实体
@OneToOne(targetEntity=Address.class)
@JoinTable(name="person_address",
	joinColumns=@JoinColumn(name="person_id" , referencedColumnName="person_id" , unique=true),
	inverseJoinColumns=@JoinColumn(name="address_id" , referencedColumnName="address_id", unique=true)
)
代码详见:codes\06\6.1\unidirectional\1-1jointable
??上面代码的架构图如下:
技术分享

3. 单向1—N关联

??单向1—N关联的持久化类发生了改变,持久化类里需要使用集合属性。因为1的一端需要访问N的一端,而N的一端以集合(Set)形式表现。从这个意义上来看,1—N关联(实际上还包含N—N)和前面的集合属性非常相似,只是此时集合里的元素是关联实体。

??对于单向的1—N关联的持久化类,只需要在1的一端增加Set类型的成员变量,该成员变量记录当前实体所有的关联实体,当然还要为这个Set类型的属性增加setter和getter方法。

3.1 无连接表的单向1—N关联

??对于无连接表的1—N单向关联而言,同样需要在N的一端增加外键列来维护关联关系,但由于程序此时只让1的一端来控制关联关系,因此直接在1的一端使用@JoinColumn修饰Set集合属性、映射外键列即可。示例代码如下:

@Entity
@Table(name="person_inf")
public class Person{
	// 标识属性
	@Id @Column(name="person_id")
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	private Integer id;
	private String name;
	private int age;
	// 定义该Person实体所有关联的Address实体,没有指定cascade属性
	@OneToMany(targetEntity=Address.class)
	// 映射外键列,此处映射的外键列将会添加到关联实体对应的数据表中
	@JoinColumn(name="person_id" , referencedColumnName="person_id")
	private Set<Address> addresses = new HashSet<>();
	...
}
代码详见:codes\06\6.1\unidirectional\1-Nnojointable
??上面代码的架构图如下:
技术分享

??上面代码中首先使用@OneToMany修饰Set集合属性,接下来使用@JoinColumn映射了外键列,此时的外键列并不是增加在当前实体对应的数据表中,而是增加到关联实体(Address)对应的数据表中。由于Address类并不需要维护与Person类的关联关系,因此Address类看上去并没有任何特别之处。

??对于Person实体类而言,程序使用@OneToMany时并没有指定cascade属性,这意味着对主表实体的持久化操作不会级联到从表实体。

使用如下代码来保存一个Person对象和两个Address对象:

private void testPerson() {
	Session session = HibernateUtil.currentSession();
	Transaction tx = session.beginTransaction();
	// 创建一个Person对象
	Person p = new Person();
	// 创建一个瞬态的Address对象
	Address a = new Address("广州天河");
	// 必须先持久化Address对象
	session.persist(a); // ①
	// 设置Person的name为crazyit.org字符串
	p.setName("crazyit.org");
	p.setAge(21);
	// 设置Person和Address之间的关联关系
	p.getAddresses().add(a);
	// 持久化Person对象
	session.save(p);
	// 创建一个瞬态的Address对象
	Address a2 = new Address("上海虹口");
	// 必须先持久化Address对象
	session.persist(a2); //  ②
	// 修改持久化状态的Person对象
	p.getAddresses().add(a2);
	tx.commit();
	HibernateUtil.closeSession();
}
??session.save(p);代码持久化了一个Person对象,且Person对象关联一个Address对象,此时Hibernate需要完成那些事情呢?

  1. 执行insert into person_inf...语句,向Person表中插入一条记录
  2. Hibernate视图执行update address_inf...语句,将当前person_inf表记录关联的address_inf表记录的外键修改为该person_inf表记录的主键值

执行过程如下:

Hibernate: 
    insert 
    into
        address_inf
        (addressDetail) 
    values
        (?)
Hibernate: 
    insert 
    into
        person_inf
        (age, name) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        address_inf
        (addressDetail) 
    values
        (?)
Hibernate: 
    update
        address_inf 
    set
        person_id=? 
    where
        address_id=?
Hibernate: 
    update
        address_inf 
    set
        person_id=? 
    where
        address_id=?

??但问题是:如果address_inf表中将要被修改的记录不存在呢(将 ①好代码注释掉),会抛出异常,异常信息如下:

Hibernate: 
    insert 
    into
        person_inf
        (age, name) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        address_inf
        (addressDetail) 
    values
        (?)
Hibernate: 
    update
        address_inf 
    set
        person_id=? 
    where
        address_id=?

Exception in thread "main" org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: 。。。。?

如果程序使用在@OneToMany注解中指定了cascade=CascadeType.All,则Hibernate所完成的事是:
  1. 执行insert into person_inf...语句,向Person表中插入一条记录
  2. 执行insert into address_inf...语句,向Address表中插入一条记录
  3. 执行update into address_inf...语句来修改刚刚插入的address_inf记录

??正如上面的程序中展现的,由于配置时没有指定cascade,因此程序对Person实体的持久化操作不会级联到关联的Address实体,程序必须显式持久化两个Address实体 ① ②代码所示。

??上面程序仅仅需要为Person实体增加一个关联Address实体,但Hibernate会采用两条SQL语句来完成:

  1. insert语句插入一条外键为null的address_inf记录
  2. update语句修改刚才插入address_inf记录

??造成这种现象的根本原因是:从Person到Address的关联(外键person_id)没有被当做Address对象状态的一部分(程序是通过把Address实体添加到Person实体的address集合属性中,而Address实体并不知道他所关联的Person实体),因而Hibernate无法执行inser into address_inf...语句时为该外键列指定值。

??解决思路:程序必须在持久化Address实体之前,让Address实体能“知道”它所关关联的Person实体,也就是通过address.setPerson(person);方法来建立 关联关系——这就需要把这个关联关系添加到Address的映射中——这就变成了双向1—N关联。因此应该尽量少用单向1—N关联,而是改为使用双向1—N关联。

注意:对于双向的1—N父子关联,使用1的一端控制关系的性能,比使用N的一端控制关系的性能低。性能低的原因是当使用1的一端控制关联关系时,由于插入数据时无法同时插入外键列,因此会额外多出一条 update语句,而外键列还无法增加非空约束——因为添加关联实体时,总会先插入一条外键列为null的记录。

3.2 有连接表的单向1—N关联

??对于有连接表的1—N关联,同样需要使用@OneToMany修饰代表关联实体的集合属性。除此之外还应使用@JoinTable显示指定连接表。

// 定义该Person实体所有关联的Address实体
@OneToMany(targetEntity = Address.class)
// 映射连接表为person_address
@JoinTable(name = "person_address",
		// 定义连接表中名为person_id的外键列,该外键列参照当前实体对应表的主键列
		joinColumns = @JoinColumn(name = "person_id", referencedColumnName = "person_id"),
		// 定义连接表中名为address_id的外键列,
		// 该外键列参照当前实体的关联实体对应表的主键列
		inverseJoinColumns = @JoinColumn(name = "address_id", referencedColumnName = "address_id", unique = true))
private Set<Address> addresses = new HashSet<>();
代码详见:codes\06\6.1\unidirectional\1-Njointable
??上面代码的架构图如下:
技术分享

??对于采用连接表的单向1—N关联而言,由于采用了连接表来维护1—N关联而言,由于采用连接表维护1—N关联关系,两个实体对应的数据表都无需增加外键列,因此不存在主从表关系,程序完全可以想先持久化哪个实体,就先持久化哪个实体。无论先持久化哪个实体,程序都不会引发性能问题。

4. 单向N—N关联

??单向的N—N关联和1—N关联的持久化类代码完全相同,控制关系的一端需要增加一个Set类型的属性,被关联的持久化实例以集合形式存在。

??N—N关联必须使用连接表,N—N关联与有连接表的1—N关联非常相似,因此都需要使用@JoinTable来映射连接表,区别是N—N关联要去掉@JoinTable注解的inverseJoinColumns属性所指定的@JoinColumn中的unique=true。

代码详见:codes\06\6.1\unidirectional\N-N

5. 双向1—N关联

??对于1—N关联,Hibernate推荐使用双向关联,而且不要让1的一端控制关联关系,而使用N的一端控制关联关系。

??双向的1—N关联与N—1关联时完全相同的两种情形,因此没有双向N—1配置。两端需要增加对关联属性的访问,N的一端增加引用到关联实体的属性,1的一端增加集合属性,集合元素为关联实体。

注意:虽然Hibernate对双向关联支持使用连接表,不过大部分使用无连接表的映射策略即可。

5.1 无连接表的双向1—N关联

??无连接表的双向1—N关联,N的一端需要增加@ManyToOne注解来修饰代表关联实体的属性,而1的一端需要使用@OneToMany注解来修饰代表关联实体的属性。

??底层数据库实际上只需要在N的一端的数据表里增加一个外键列即可,因此应该在使用@ManyToOne注解的同时,使用@JoinColumn来映射外键列。

??对于双向1—N关联映射,通常不应该在1的一端控制关联关系,而应该有N的一端来控制关联关系,因此应该在使用@OneToMany注解时指定mappedBy属性——一旦为@OneToMany、@ManyToMany指定了该属性,则表明当前实体不能控制关联关系。当@OneToMany、@ManyToMany、@OneToOne所在的当前实体放弃控制关联关系之后,Hibernate就不允许使用@JoinColumn或@JoinTable修饰代表关联实体的属性了。

实例如下:

@Entity
@Table(name = "person_inf")
public class Person {
	// 标识属性
	@Id
	@Column(name = "person_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private String name;
	private int age;
	// 定义该Person实体所有关联的Address实体
	// 指定mappedBy属性表明该Person实体不控制关联关系
	@OneToMany(targetEntity = Address.class, mappedBy = "person")
	private Set<Address> addresses = new HashSet<>();
	...
}
??Person类中增加了一个Set集合属性,用于记录它所关联的一系列Address实体,程序使用了@OneToMany修饰该集合属性,表明此处是1—N关联。使用了mappdBy属性,表明Person实体不控制关联关系。而Address端则只需要增加一个Person类型的属性,这表明Address和Person存在N—1的关联关系。

@Entity
@Table(name = "address_inf")
public class Address {
	// 标识属性
	@Id
	@Column(name = "address_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int addressId;
	// 定义地址详细信息的成员变量
	private String addressDetail;
	// 定义该Address实体关联的Person实体
	@ManyToOne(targetEntity = Person.class)
	// 定义名为person_id外键列,该外键列引用person_inf表的person_id列。
	@JoinColumn(name = "person_id", referencedColumnName = "person_id", nullable = false)
	private Person person;
...
}
??Address类中使用了@ManyToOne修饰代表关联实体的属性,也使用了@JoinColumn映射外键列,这意味着address_inf表将作为从表使用。
代码详见:codes\06\6.1\bidirectional\1-Nnojointable

??上面代码的架构图如下:

技术分享

为了保证较好的性能,主程序需要注意如下几点:

  • 最好先持久化Person对象(或该Person对象本身已处于持久化状态)。因为程序希望持久化Address对象时,Hibernate可为Address的外键属性分配值——也就是说,向address_inf数据表插入记录时,该记录的外键列已指定了值——这表明它参照的主表记录已存在,也就是Person对象必须已持久化。
  • 先设置Person和Address的关联关系(setter),再保持持久化Address对象。如果顺序反过来,程序持久化Address对象时,该Address对象还没有关联实体,所以Hibernate不可能为对应记录的外键列指定值;等到设置关联关系时,Hibernate只能再次使用Update语句来修改关联关系。
  • 不要通过Person对象来设置关联关系,因为已经在Person映射注解@OneToMany中指定了mappedBy属性,该属性表明Person对象不能控制关联关系。

5.2 有连接表的双向1—N关联

??对于有连接表的双向1—N关联而言,1的一端无须任何改变;只要在N的一端使用@JoinTable显式指定连接表即可。

??对于使用连接表的1—N关联,Person类(1的一端)与不适用连接表的1—N关联中Person类的源码相同。而Address类则需要使用@JoinTable映射连接表。

@Entity
@Table(name = "address_inf")
public class Address {
	// 标识属性
	@Id
	@Column(name = "address_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int addressId;
	// 定义地址详细信息的成员变量
	private String addressDetail;
	// 定义该Address实体关联的Person实体
	@ManyToOne(targetEntity = Person.class)
	// 映射连接表,指定连接表为person_address
	@JoinTable(name = "person_address",
			// 指定连接表中address_id列参照当前实体对应数据表的address_id主键列
			joinColumns = @JoinColumn(name = "address_id", referencedColumnName = "address_id", unique = true),
			// 指定连接表中person_id列参照当前实体的关联实体对应数据表的person_id主键列
			inverseJoinColumns = @JoinColumn(name = "person_id", referencedColumnName = "person_id"))
	private Person person;
...
}
??对于上面的这种映射方式,由于程序依然在Person一端的@OneToMany注解中指定了mappedBy属性,这意味着该Person实体依然不能控制关联关系。
??上面代码的架构图如下:

技术分享

代码详见:codes\06\6.1\bidirectional\1-Njointable

??如果程序希望Person实体也可控制关联关系—对于有连接表的1—N关联而言,使用1的一端控制关联关系,并不会影响程序性能,name程序就需要删除@OneToMany注解的mappedBy属性,并同时配合使用@JoinTable注解。如下:

@Entity
@Table(name = "person_inf")
public class Person {
	// 标识属性
	@Id
	@Column(name = "person_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private String name;
	private int age;
	// 定义该Person实体所有关联的Address实体
	// @OneToMany(targetEntity=Address.class ,mappedBy="person")
	@OneToMany(targetEntity = Address.class)
	// 映射连接表,指定连接表为person_address
	@JoinTable(name = "person_address",
			// 指定连接表中person_id列参照当前实体对应数据表的person_id主键列
			joinColumns = @JoinColumn(name = "person_id", referencedColumnName = "person_id"),
			// 指定连接表中address_id列参照当前实体的关联实体对应数据表的address_id主键列
			inverseJoinColumns = @JoinColumn(name = "address_id", referencedColumnName = "address_id", unique = true))
	private Set<Address> addresses = new HashSet<>();
	...
}
代码详见:codes\06\6.1\bidirectional\1-Njointable

??上面代码的架构图如下:
技术分享

??上面Person类中注解也指定了连接表的表名为person_address——要求1的一端、N的一端的@JoinTable的name属性相同,这就保证它们映射的是同一个连接表,两端使用@JoinTable时指定的外键列的列名也是相互对应的。这种映射方式,程序即可让Person实体控制关联关系,也可让Address实体控制关联关系,而且由于Person实体、Address实体的关联关系交给连接表管理,因此他们不存在主从表关系。因此,程序完全可以想先持久化哪个实体,就先持久化哪个实体。无论先持久化哪个实体,程序都不会引发性能问题。

6. 双向N—N关联

??双向N—N关联需要两端都使用Set集合属性,两端都增加对集合属性的访问。双向N—N关联没有太多选择,只能采用连接表来建立两个实体之间的关联。

??双向N—N关联需要在两端分别使用@ManyToMany修饰Set集合属性,并在两端都使用@JoinTable显示映射连接表。在两端映射连接表时,两端指定的连接表的表名应该相同,而且两端使用@JoinTable时指定的外键列的列名也是相互对应的。

注:如果一端放弃控制关联关系,则可在这一端的@ManyToMany注解中指定mappedBy属性,且不能使用@JoinTable映射连接表了。

示例:

@Entity
@Table(name = "person_inf")
public class Person {
	// 标识属性
	@Id
	@Column(name = "person_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private String name;
	private int age;
	// 定义该Person实体所有关联的Address实体
	@ManyToMany(targetEntity = Address.class)
	// 映射连接表,指定连接表的表名为person_address
	@JoinTable(name = "person_address",
			// 映射连接表中名为person_id的外键列,
			// 该列参照当前实体对应表的person_id主键列
			joinColumns = @JoinColumn(name = "person_id", referencedColumnName = "person_id"),
			// 映射连接表中名为address_id的外键列,
			// 该列参数当前实体的关联实体对应表的address_id主键列
			inverseJoinColumns = @JoinColumn(name = "address_id", referencedColumnName = "address_id"))
	private Set<Address> addresses = new HashSet<>();
...
}
@Entity
@Table(name = "address_inf")
public class Address {
	// 标识属性
	@Id
	@Column(name = "address_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int addressId;
	// 定义地址详细信息的成员变量
	private String addressDetail;
	// 定义该Address实体所有关联的Person实体
	@ManyToMany(targetEntity = Person.class)
	// 映射连接表,指定连接表的表名为person_address
	@JoinTable(name = "person_address",
			// 映射连接表中名为address_id的外键列,
			// 该列参照当前实体对应表的address_id主键列
			joinColumns = @JoinColumn(name = "address_id", referencedColumnName = "address_id"),
			// 映射连接表中名为person_id的外键列,
			// 该列参照当前实体对应表的person_id主键列
			inverseJoinColumns = @JoinColumn(name = "person_id", referencedColumnName = "person_id"))
	private Set<Person> persons = new HashSet<>();
...
}
??上面代码的架构图如下:

技术分享

7. 双向1—1关联

??双向1—1关联关联,两端的持久化类都增加引用关联实体的属性,并为该属性提供setter和getter方法。

7.1 基于外键的双向1—1关联

??对于双向1—1关联而言,两端都需要使用@OneToOne注解进行映射。对于基于外键的双向1—1关联,未安检可以存放在任意一端。存放外键的一端,需要增加@JoinColumn注解来映射外键列,并且为@JoinColumn注解增加unique=true属性来标识该实体实际上1的一端。

??对于1—1的关联关系,两个实体原本处于平等状态;但当选择任意一端来增加外键后,该表即变成从表,而另一个表则成为主表。对于1—1关联关联的主表对应的实体,不应该用于控制关联关系(否则会导致生成额外的Update语句,从而引起性能下降),因此主表对应的实体中使用@OneToOne注解时,应增加mappedBy属性(该属性用于表明该实体不管理关联关系,这一端对应的数据表将作为主表使用,不能使用@JoinColumn映射外键列)

示例:(Person为主表)

@Entity
@Table(name = "person_inf")
public class Person {
	// 标识属性
	@Id
	@Column(name = "person_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private String name;
	private int age;
	// 定义该Person实体关联的Address实体
	@OneToOne(targetEntity = Address.class, mappedBy = "person")
	private Address address;
...
}
@Entity
@Table(name = "address_inf")
public class Address {
	// 标识属性
	@Id
	@Column(name = "address_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int addressId;
	// 定义地址详细信息的成员变量
	private String addressDetail;
	// 定义该Address实体关联的Person实体
	@OneToOne(targetEntity = Person.class)
	// 用于映射person_id外键列,参照person_inf表的person_id列
	// 指定了unique=true表明是1-1关联
	@JoinColumn(name = "person_id", referencedColumnName = "person_id", unique = true)
	private Person person;
...
}
??上面代码的架构图如下:

技术分享

7.2 有连接表的双向1—1关联

??采用连接表的双向1—1关联关联相当罕见的情形,映射相当复杂,数据模型繁琐。通常不推荐使用这种策略。

示例如下:

@Entity
@Table(name = "person_inf")
public class Person {
	// 标识属性
	@Id
	@Column(name = "person_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private String name;
	private int age;
	// 定义该Person实体关联的Address实体
	@OneToOne(targetEntity = Address.class)
	// 映射底层连接表,表名为person_address
	@JoinTable(name = "person_address",
			// 映射连接表的外键列,增加unique=true表明是1-1关联
			joinColumns = @JoinColumn(name = "person_id", referencedColumnName = "person_id", unique = true),
			// 映射连接表的外键列,增加unique=true表明是1-1关联
			inverseJoinColumns = @JoinColumn(name = "address_id", referencedColumnName = "address_id", unique = true))
	private Address address;
...
}
@Entity
@Table(name = "address_inf")
public class Address {
	// 标识属性
	@Id
	@Column(name = "address_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int addressId;
	// 定义地址详细信息的成员变量
	private String addressDetail;
	// 定义该Address实体关联的Person实体
	@OneToOne(targetEntity = Person.class)
	// 映射底层连接表,表名为person_address
	@JoinTable(name = "person_address",
			// 映射连接表的外键列,增加unique=true表明是1-1关联
			joinColumns = @JoinColumn(name = "address_id", referencedColumnName = "address_id", unique = true),
			// 映射连接表的外键列,增加unique=true表明是1-1关联
			inverseJoinColumns = @JoinColumn(name = "person_id", referencedColumnName = "person_id", unique = true))
	private Person person;
...
}
??上面代码的架构图如下:

技术分享

8. 复杂关联

8.1 组件属性包含的关联实体

带完善——》》》》》预计在月底完成

8.2 基于复合主键的关联关系

带完善——》》》》》预计在月底完成

8.3 复合主键的成员属性为关联实体

带完善——》》》》》预计在月底完成

9. 简要总结

??单向与双向是针对面向对象中的实体类的,数据库底层并没有这一点。简单来说,单向就是两个实体类中,一个实体类有另外一个实体类的引用声明。双向即都互相包含。

??单向的话无论是那种方式,总是前者控制关联关系,即保存着关联表对应的实体类额引用。

??其中@JoinColumn是用来定义外键的,而外键在哪个表,哪个就是从表,相对应的哪个表即为主表。

??而@JoinTable是用来定义中间表的,其中joinColumns和inverseJoinColumns属性,都是用来定义中间表的外键的。通常情况下,在对应的实体类中定义其对应的外键列的时候使用joinColumns,该数据库底层实现的时候也被定义为中间表的主键,主键当然具有唯一性了,所以定义不定义unique = true属性都无所谓;而inverseJoinColumns属性,如果需要指定外键列的唯一性,则必须指定unique = true属性。

??前面所提及的1与N,在定义时其实就是:1为唯一性,N不具备唯一性。而定义唯一性使用的是各JoinColumn注解中的unique = true。因为是外键将两个表关联起来的,因此通过外键与关联表的字段综合判断。使用中间表为例比较明了的进行解释:(主表以及从表中,与中间表有关联字段是主键或唯一性约束,都具有唯一性;反之则为N),中间表有两个字段,其中肯定至少有一个是主键,若另外一个字段不是主键或不是唯一性约束,则连接表主键的对应关系为1,另一个字段为N;若中间表的两个字段都为主键,其没有唯一性约束,都会变成N的关系,那一列加上唯一性约束,则其对应关系为1。(简而言之:如果单独一列主键是可以保证唯一性的,但是使用联合主键就无法保证唯一性了,如果还需要保证该字段的唯一性,则必须指定unique = true,来指明该列是具有唯一性的)

注意:此篇文章中使用的注解都位于 javax.persistence包中,可查阅JavaEE的API。但是@Cascade注解是Hibernate中的,用于指定级联操作策略,其作用与JavaEE提供的@*To*注解的属性cascade的功能完全一样,只不过导入的包不同,这一点需要注意。

@*To*注解对应生成外键所在表
注解 外键所在表
@ManyToOne 当前类所对应的数据库表
@OneToOne 当前类所对应的数据库表
@OneToMany 当前类所关联实体所对应的数据库表
注:上面的表格只是针对外键位置所进行的说明,还需要配合上面加黑的《使用中间表为例比较明了的进行解释》一同进行理解才可以。

??控制关联关系,其实是在级联的时候是否自动进行关联。如果放弃的话,Hibernate就不会去自动的去关联去了,必须手动进行关联。

9.1 无连接表

9.1.1 N—1关联

??无论单双向,都需要在N的一端使用@ManyToOne注解修饰代表关联实体的属性。

??单向N—1关联,程序只要在N的一端增加一列外键,让外键值记录该对象所属的实体即可,而外键会保存到当前类所映射的表,而外键没有唯一约束(这样底层数据表就形成了N的概念)

??没有双向N—1关联,因为理论上双向N—1关联与双向1—N关联是完全一样的。

注意:N的一端保存其关联表对应的实体类,也在该引用上使用@ManyToOne注解。

9.1.2 1—N关联

??对于1—N关联,Hibernate推荐使用双向关联,而且不要让1的一端控制关联关系,而使用N的一端控制关联关系。
??双向的1—N关联与N—1关联时完全相同的两种情形,因此没有双向N—1配置。双向的关联的两端都需要增加对关联属性的访问,N的一端增加引用到关联实体的属性,1的一端增加集合属性,集合元素为关联实体。
注意:虽然Hibernate对双向关联支持使用连接表,不过大部分使用无连接表的映射策略即可。

??单向1—N关联,持久化类中需要使用集合属性。因为1的一端需要访问N的一端,而N的一端将以集合(Set)的形式在1的持久化类中表现,从这个意义上来看,1—N(包含N—N)和集合属性非常相似,只是此时集合里的元素时关联实体。也就是说:在1的一端增加Set类型的成员变量并使用@OneToMany注解进行修饰,该成员变量记录当前实体所有的关联实体,当然还要为这个Set类型的属性增加setter和getter方法。并在该Set类型的成员变量上定义外键,并且unique为默认值(false),此处映射的外键列将会添加到关联实体对应的数据表中(也就是说此时外键不在当前当前实体类所映射的数据库表中)

??双向1—N关联,N的一端需要增加@ManyToOne注解来修饰代表关联实体的属性,而1的一端需要使用@OneToMany注解来修饰代表关联实体的属性。底层数据库为了记录这种1—N关联关系,实际上只需要在N的一端数据表里增加一个外键即可,因此应该在使用@ManyToOne注解的同时,使用@JoinColumn来映射外键列。

注意:如果不要让1端的控制关联关系,则需要使用@OneToMany注解的mappedBy属性。

9.1.3 1—1关联

??对于单向基于外键1—1关联而言,只要先使用@OneToOne注解修饰代表关联实体的属性,再使用@JoinColumn映射外键即可。注:由于是1—1关联,因此应该为@JoinColumn增加unique=true。如果你不添加这个属性,就会变成单向的N—1关系。

??对于双向基于外键1—1关联而言,两端都需要使用@OneToOne注解进行映射,而外键可以存放在任意一端。存放外键的一端,需要增加@JoinColumn注解来映射外键列,而且还应该为@JoinColumn注解增加unique=true属性来表示该实体实际上是1的一端。只不过增加外键就变成了从表,而主表对应的实体不应该用于控制关联关系(否则会导致生成额外的Update语句)

9.1.4 N—N关联

??N—N关联无论是单向或双向,都必须使用中间表。??

9.2 使用关联表

9.2.1 N—1关联

??对于带部分来说,使用基于外键的关联映射已经足够了。而使用关联表只需要在@ManyToOne注解下使用@JoinTable定义中间表即可。

??需要注意的是@JoinTable注解的inverseJoinColumns属性定义外键的时候,其@JoinColumn的unique属性,应该采用默认的false值,否则就变成了一对一了。

注意:N的一端保存其关联表对应的实体类,也在该引用上使用@ManyToOne注解。

9.2.2 1—N关联

??单向1—N关联,同样需要使用@OneToMany修饰代表关联实体的集合属性,除此之外,程序还应使用@JoinTable显示指定连接表。注意:@JoinTable注解的inverseJoinColumns属性定义外键的时候,其@JoinColumn的unique属性,应该采用默认的false值。(当然1的一端为了保证唯一需要制定unique属性,而JoinTable的两个属性中JoinColumns制定的外键已经在其表中是主键了,已然是唯一的了)

??双向1—N关联,1的一端需要使用@OneToMany注解来修饰代表关联实体的属性,只要在N的一端使用@JoinTable显示指定连接表即可。注意:@JoinTable注解的inverseJoinColumns属性定义外键的时候,其@JoinColumn的unique属性,应该采用默认的false值。

9.2.3 1—1关联

??有连接表的1—1单向关联同样需要显式使用@JoinTable映射连接表,除此之外,由于此处的@JoinTable映射的连接表维护的是1—1关联,因此程序需要为@JoinTable中JoinColumn属性映射的外键列增加unique=true,也为inverseJoinColumn属性的外键列增加=true。

??有连接表的1—1双向关联在两端分别使用@OneToOne修饰代表关联实体的属性,并在两端都使用@JoinTable显式映射连接表。

9.2.4 N—N关联

??单向N—N关联,需要在控制关系的一端增加一个Set类型的属性,并使用@ManyToMany注解来修饰代表关联实体的集合属性,于1—N关联非常相似,都需要使用@JoinTable来映射连接表,区别是N—N关联要去掉@JoinTable注解的inverseJoinColumn属性所指定的@JoinColumn中的unique=true。

??双向N—N关联,需要爱两端都使用Set集合属性,两端都增加对集合属性的访问,并且需要在两端分别使用@ManyToMany修饰Set集合属性,并在两端都使用@JoinTable显示映射连接表。注:如果有一段希望放弃控制关联关系,则可在这一端的@ManyToMany注解中指定mappedBy属性。

10. 持久化的传播性

??当程序中有两个关联实体时,程序需要主动保存、删除或重关联每个持久化实体;如果需要处理许多彼此关联的实体,则需要依次保存每个实体。这会让人感觉有点烦琐。

??从数据库建模的角度来看,两个表之间的1—N关联关系总是用外键约束来表示,其中保留外键的数据表称为从表,被从表参照的数据表称为主表。对于这种主从表约束关系,Hibernate则由两种映射策略:

  • 将从表记录映射成持久化类的组件,也就是集合属性的集合元素是组件
  • 将从表记录也映射成持久化实体,这就是此处介绍的关联关系

??如果将从表记录映射成持久化类的组件,这些组件的声明周期总是依赖于父对象,Hibernate会默认启用级联操作,不需要额外的动作。当父对象被保存时,这些组件子对象也将被保存;父对象被删除时,子对象也将被删除。

??如果将从表记录映射成持久化实体,则从表实体也有了自己的生命周期,从而应该允许其他实体共享对它的引用。例如,从集合中移除一个实体,不意味着它可以被删除。所以Hibernate默认不启用实体到其他关联实体之间的级联操作。

??对于关联实体而言,Hibernate默认不会启用级联操纵,当父对象被保存时,它所关联的子对象不会被保存;父对象被删除时,他关联的子实体不会被删除。为了启用不同持久化操作的级联行为,Hibernate定义了如下级联风格:

  • CascadeType.ALL:指定Hibernate将所有的持久化操作都级联到关联实体
  • CascadeType.MERGE:指定Hibernate将merge操作级联到关联实体(更新)
  • CascadeType.PERSIST:指定Hibernate将persist操作级联到关联实体(保存)
  • CascadeType.REFRESH:指定Hibernate将refresh操作级联到关联实体(将数据库的数据更新到本地的实体中,而不是本地实体更新数据到数据库中)
  • CascadeType.REMOVE:指定Hibernate将remove操作级联到关联实体(删除)

??如果程序希望某个操作能被级联传播到关联实体,则可以在@OneToMany、@OneToOne、@ManyToMany时通过cascade属性来指定。可以使用cascade = CascadeType.ALLCascadeType.ALL指定所有的持久化操作都被级联到关联实体,Hibernate对关联实体默认不使用任何级联,即任何操作都不会被级联到关联实体。

??Hibernate还支持一个特殊的级联策略:删除“孤儿”记录(可通过@OneToMany、@OneToOne的orphanRemoval属性来启动该级联策略),该级联策略只对当前实体是1的一端,其底层数据表为主表时有效。对于启用了orphanRemoval策略的级联操作而言,当程序通过主表实体切断与从表实体的关联关系时——虽然此时主表实体对应的记录并没有删除,但由于从表实体失去了对主表实体的引用,因此这些从表实体就变成了“孤儿”记录,Hibernate会自动删除这些记录。

对于级联策略的设定,Hibernate有如下建议:

  • 在@ManyToOne中指定级联没什么意义。级联操作通常在@OneToOne和@OneToMany关系中比较有用——因为级联操作是由主表记录传播到从表记录。但在某些极端情况下,如果程序就是希望为@ManyToOne指定级联策略,则也可使用Hibernate提供的@Cascade注解。
  • 如果从表记录被完全限制在主表记录之内(当主表记录被删除后,从表记录没有存在的意义),则可以指定cascade=Cascade.ALL,在配合orphanRemoval=true级联策略,将从表实体的生命周期完全交给主表实体管理。
  • 如果经常在某个事务中同时使用主表实体和从表实体,则可以考虑指定cascade={CascadeType.PERSIST,CascadeType.MERGE}级联策略

10.1 cascade=Cascade.ALL与orphanRemoval=true

??cascade=Cascade.ALL级联策略的详细解释:

  • 如果主表实体被persist(),name关联的从表实体也会被persist()
  • 如果主表实体被merge(),name关联的从表实体也会被merge()
  • 如果主表实体被save(),update(),或saveOrUpdate(),那么所有的从表实体则会被saveOrUpdate()
  • 如果把持久化状态下的主表实体和瞬态或托管的从表实体建立关联,则从表实体将自动持久化
  • 如果主表实体被删除,那么关联的从表实体也会被删除
  • 如果没有把主表实体删除,只是切断主表实体和从表实体之间的关联关系,则关联的从表实体不会被删除,只是将关联的从表实体的外键列设为null

??如果指定了orphanRemoval=true策略,则只要一个从表实体失去了关联的主表实体,不管该主表实体是被删除,还是切断了主表实体和它的关联,name该从表实体就变成了orphan(孤儿),Hibernate将自动删除从表实体。

10.2 深度剖析

??所有操作都是在调用期(call time)或者写入期(flush time)级联到关联对象上的。如果可能,Hibernate通常会在调用持久化操作时(调用期)将持久化操作级联到关联实体上。然而,save-update和orphanRemoval操作是在Session flush时(写入期)才级联到关联对象上的。

??Hibernate的级联风格看上去比数据库本身的级联操作更加强大,这是因为Hibernate的级联操作是建立在程序级别上的,而数据库的级联操作则是建立在数据库级别上的。

&#8195&#8195

参考资料:

《轻量级JavaEE企业应用实战 第四版》

最后修改时间:2017年4月15日00:17:18

************************************************************************结束语************************************************************************
??我在写这篇博客的时候也是一名初学者,有任何疑问或问题请留言,或发邮件也可以,邮箱为:fanxiaobin.fxb@qq.com,我会尽早的进行更正及更改。
在我写过的博客中有两篇博客是对资源的整理,可能对大家都有帮助,大家有兴趣的话可以看看!!
下载资料整理——目录http://blog.csdn.net/fanxiaobin577328725/article/details/51894331
??这篇博客里面是我关于我见到的感觉不错的好资源的整理,里面包含了书籍及源代码以及个人搜索的一些资源,如果有兴趣的可以看看,我会一直对其进行更新和添加。
优秀的文章&优秀的学习网站之收集手册http://blog.csdn.net/fanxiaobin577328725/article/details/52753638
??这篇博客里面是我对于我读过的,并且感觉有意义的文章的收集整理,纯粹的个人爱好,大家感觉有兴趣的可以阅读一下,我也会时常的对其进行更新。
************************************************************************感谢************************************************************************

Hibernate学习笔记之关联关系

标签:注释   ble   持久化类   也会   type   alt   details   现象   专业   

原文地址:http://blog.csdn.net/fanxiaobin577328725/article/details/70053850

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!