Hibernate的关联映射
客观世界中很少有对象是独立存在的,比如我们可以通过某个老师获取该老师教的所有学生,我们也可以通过某个学生获得教他的对应的老师,实体之间的互相访问就是关联关系。
在Hibernate中有两种关联关系,即单向关联和双向关联。
单向关联:只能单向访问关联端,如只能通过老师访问学生。
双向关联:关联的两端可以互相访问,如老师可以访问学生,学生可以访问老师。
单向关联可以分为:1->1 1->N N->1 N-N
双向关联可以分为:1-1 1-N N-N
1.单向N->1关联
单向N->1是指可以通过N中的某一个找到1中的实体,所以控制的那一端在N端,使用的是@ManyToOne注解。
单向N->1关系,比如多个人对应一个地址,可以从人的这一端找到对应的地址实体,不需要关心某个地址的用户。
无连接表的N->1关联
Person.java
@Entity
@Table(name="person_info")
public class Person {
@Id
@Column(name="person_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private int age; @ManyToOne(targetEntity=Address.class)
@JoinColumn(name="address_id",nullable=false)
@Cascade(CascadeType.ALL)
private Address address;
...//此处省略getter/setter等其他方法
}
Address.java
@Entity
@Table(name="address_info")
public class Address {
@Id
@Column(name="address_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private int addressId;
private String adressDetail;
...//此处省略getter/setter/构造器等方法
}
(1)@ManyToOne注解:
对于无连接表的N->1关联而言,只需要在N的那一端添加@ManyToOne注解,1的那一端作为一个一般的实体类就可。我们用targetEntity属性指定关联实体的类名,大部分时候无需指定targetEntity,但是如果使用@OneToMany或@ManyToMany修饰的1--N、N--N关联,使用Set集合不带泛型信息,就必须指定targetEntity属性
(2)@JoinColumn注解:
@JoinColumn表示通过外键关联策略进行映射,也就是说,程序在N的这一端增加一列外键,让外键记录该对象所属的实体,@JoinColumn用于映射底层的外键列。
(3)cascade注解:指定Hibernate对关联实体采用怎样的级联策略,包括以下五种情况:
CascadeType.ALL:将所有的持久化操作都级联到关联实体
CascadeType.MERGE:将merge操作都级联到关联实体
CascadeType.PERSIST:将persist操作都级联到关联实体
CascadeType.REFRESH:将refresh操作都级联到关联实体
CascadeType.REMOVE:将所remove操作都级联到关联实体
(4)fetch:指定抓取关联实体时的抓取策略
FetchType.EAGER:立即抓取
FetchType.LAZY:延迟抓取
配置文件hibernate.cxf.xml加上:
<hibernate-configuration>
<session-factory>
<property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql:///mydb</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">123456</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<property name="show_sql">true</property>
<property name="hibernate.format_sql">false</property>
<mapping class="myHibernate.Person"/>
<mapping class="myHibernate.Address"/>
</session-factory>
</hibernate-configuration>
测试:
public class myTest {
public static void main(String[] args) {
Configuration config=new Configuration().configure();
SessionFactory factory=config.buildSessionFactory();
Session session=factory.openSession();
Transaction tx=session.beginTransaction();
Person p=new Person();
p.setName("lyy");p.setAge(22);
//创建一个瞬态的Address对象
Address a=new Address("北京");
//session.save(a);
p.setAddress(a);
session.save(p);
//再次创建一个瞬态的Address对象
Address a2=new Address("南京");
p.setAddress(a2);
tx.commit();
session.close();
}
}
程序执行到创建一个瞬态的Address对象时,我们知道,当某一个表中有关联表时,通常是要先保存(删除)关联表Address,然后再保存(删除)从表Person。也就是说,先有session.save(a);然后再有 session.save(p);但是如果我们把session.save(a);去掉,程序会有什么后果呢。这个时候,由于关联表Address还处于瞬态,没有保存,通常情况下会有两种情况出现:
(1)程序抛出异常:TransientObjectException:object references an unsaved transient instance - save the transient instance before flushing
(2)系统自动级联插入主表记录,再插入从表记录。
在上个例子中,程序会出现第二种情况,因为我们指定了@Cascade(CascadeType.ALL)。这意味着系统将会自动级联插入主表记录,即先持久化Address对象,再持久化Person对象。也就是说,Hibernate先执行了一条insert into address...语句,再执行一条insert into person...语句。也就是说,当实体类中没有@Cascade(CascadeType.ALL)这个注解,程序就会报(1)中的异常。
当程序再次创建一个瞬态的Address对象时,程序会将该瞬态的Address对象关联到已经持久化的Person对象中,注意,这里的Person对象已经save过,是一个持久化的对象,不需要再保存了,重新关联了新的Address对象就相当于是执行了一次更新操作。也就是说,hibernate先执行了一条insert into address...语句,又执行了update person...语句。此时数据库中address表有两列数据,person表有一列数据。
有连接表的N->1关联
通常情况下,我们都是使用基于外键的关联映射,很少用有连接表的关联映射,所以,只在N->1关联中举一个例子。
将Person.java修改为:
@Entity
@Table(name="person_info")
public class Person {
@Id
@Column(name="person_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private int age; @ManyToOne(targetEntity=Address.class)
@JoinTable(name="person_address", //此处指定连接表的表名为person_address
//指定连接表中person_id外键列,参照当前实体对应表的主键列
joinColumns=@JoinColumn(name="person_id",referencedColumnName="person_id",unique=true),
//指定连接表的address_id外键列,参照当前实体的关联实体对应表的主键列
inverseJoinColumns=@JoinColumn(name="address_id",referencedColumnName="address_id"))
@Cascade(CascadeType.ALL)
private Address address;
...
}
joinColumns:可接受多个@joinColumn,用于配置连接表中外键列的列信息,这些外键列参照当前实体对应表的主键列
inverseJoinColumns:可接受多个@joinColumn,用于配置连接表中外键列的列信息,这些外键列参照当前实体的关联实体对应表的主键列
2.单向1->1关联
基于外键的单向1->1关联
只需要在控制的那个实体中使用@OneToOne注解,使用@JoinColumn映射外键列即可。由于是1->1关联,因此还需要为@JoinColumn增加unique=true即可。
person.java
@Entity
@Table(name="person_info")
public class Person {
@Id
@Column(name="person_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private int age; @OneToOne(targetEntity=Address.class)
@JoinColumn(name="address_id",referencedColumnName="address_id",unique=true)
@Cascade(CascadeType.ALL)
private Address address;
...
}
3.单向1->N关联
持久化类发生了改变,因为1的一端要访问N的一端,也就是说,通过一个person找到所有的address,这里的address就需要使用集合属性。也就是说,只需要在1的那一端增加Set类型的成员变量。
person.java
@Entity
@Table(name="person_info")
public class Person {
@Id
@Column(name="person_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private int age; @OneToMany(targetEntity=Address.class)
@JoinColumn(name="person_id",referencedColumnName="person_id")
private Set<Address> addresses=new HashSet<>();
...
}
注意,使用@JoinColumn映射外键列,不同的是,此处的外键列并不是增加到当前实体对应的数据表中,而是增加到关联实体Adress对应的数据表中。比如说,在单向N->1关联中,person表有id字段,name字段,age字段,还有address_id字段进行外键关联。id不同的person可能有同一个address_id。而在1->N关联中,person表并没有address_id字段,而在address表中,有person_id字段进行关联。其实很好理解,id不同的address可能有同一个person_id。如果将外键关联加到person表中,会发现很有很多个相同的person_id,主键不唯一。
测试:
public class myTest {
public static void main(String[] args) {
Configuration config=new Configuration().configure();
SessionFactory factory=config.buildSessionFactory();
Session session=factory.openSession();
Transaction tx=session.beginTransaction();
Person p=new Person();
Address a=new Address("北京");
session.persist(a);
p.setName("lyy");p.setAge(22);
p.getAddresses().add(a);
session.save(p);
Address a2=new Address("南京");
session.persist(a2);
p.getAddresses().add(a2);
tx.commit();
session.close();
}
}
分析一下执行过程,我们就会发现,由于先自动保存关联实体address,而此时address实体中的person_id是无值的,也就是说,程序先通过insert语句插入了一条外键为null的address记录,然后再执行update语句更新刚刚的插入的address记录,这肯定会影响系统性能。应该尽量少用单向1->N关联,而是改为双向1-N关联。而对于双向的1-N关联,使用1的一端控制关联关系会有很多弊端,比如插入数据时无法插入外键列,会额外多出一条update语句,并且外键列还不能增加非空约束。
4.单向N->N关联
控制的一端需要添加一个Set类型的属性,被关联的持久化实例以集合形式存在。N-N关联必须使用关联表。
person.java
@Entity
@Table(name="person_info")
public class Person {
@Id
@Column(name="person_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private int age; @ManyToMany(targetEntity=Address.class)
@JoinTable(name="person_address",
joinColumns=@JoinColumn(name="person_id",referencedColumnName="person_id"),
inverseJoinColumns=@JoinColumn(name="address_id",referencedColumnName="address_id"))
private Set<Address> addresses=new HashSet<>();
...
}
二 双向关联部分
1.双向1-N关联
对于1-N关联,Hibernate推荐使用双向1-N关联,而且用N的一端控制关联关系,使用无连接表的映射策略即可。
N的一端需要增加@ManyToOne注解,并用@JoinColumn映射外键列,而在1的一端使用@OneToMany注解,并指定mappedBy属性,一旦指定了该属性,就说明当前实体不能控制关联关系。
注意,对于指定了mappedBy的@OneToMany,@ManyToMany,@OneToOne注解,都不能与@JoinColumn或@JoinTable同时修饰代表关联实体的属性。
Person.java
@Entity
@Table(name="person_info")
public class Person {
@Id
@Column(name="person_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private int age; @OneToMany(targetEntity=Address.class,mappedBy="person")
private Set<Address> addresses=new HashSet<>();
...
}
Address.java
@Entity
@Table(name="address_info")
public class Address {
@Id
@Column(name="address_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private int addressId;
private String adressDetail;
@ManyToOne(targetEntity=Person.class)
@JoinColumn(name="person_id",referencedColumnName="person_id",nullable=false)
//这里与单向1->N不同,person_id不可以为null
private Person person;
...
}
测试:
public class myTest {
public static void main(String[] args) {
Configuration config=new Configuration().configure();
SessionFactory factory=config.buildSessionFactory();
Session session=factory.openSession();
Transaction tx=session.beginTransaction();
Person p=new Person();
p.setName("lyy");p.setAge(22);
session.save(p); //持久化person对象
Address a=new Address("北京");
a.setPerson(p); //设置person和address之间的关联关系
session.persist(a); //持久化address对象
Address a2=new Address("南京");
a2.setPerson(p);
session.persist(a2);
tx.commit();
session.close();
}
}
由程序可以发现,最好先持久化person对象,因为程序希望在持久化address对象时,可以为address的外键列person_id分配值。
2.双向N-N关联
双向N-N关联需要在两端都使用Set集合属性,两端都增加对集合属性的访问,只能采用连接表来建立两个实体之间的关联关系。
也就是说,两端都要使用@ManyToMany修饰Set集合属性,并在两端都使用@JoinTable显示映射连接表,并且两端指定的连接表的表名应该相同,指定的外键列的列名相互对应。
如果程序希望某一端放弃控制关联关系,则可以在这一端的@ManyToMany注解中指定MappedBy属性,这一端也就不能再指定@JoinTable映射连接表了。
Person.java
@Entity
@Table(name="person_info")
public class Person {
@Id
@Column(name="person_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private int age; @OneToMany(targetEntity=Address.class)
@JoinTable(name="person_address",
joinColumns=@JoinColumn(name="person_id",referencedColumnName="person_id"),
inverseJoinColumns=@JoinColumn(name="address_id",referencedColumnName="address_id",unique=true))
private Set<Address> addresses=new HashSet<>();
...
}
Address.java
@Entity
@Table(name="address_info")
public class Address {
@Id
@Column(name="address_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private int addressId;
private String adressDetail;
@ManyToOne(targetEntity=Person.class)
@JoinTable(name="person_address",
joinColumns=@JoinColumn(name="address_id",referencedColumnName="address_id"),
inverseJoinColumns=@JoinColumn(name="person_id",referencedColumnName="person_id"))
private Person person;
...
}
注意,由于此处管理的是N-N关联,就不能为@JoinColumn注解增加unique=true。
3.双向1-1关联
两端都要使用@OneToOne注解进行映射,外键可以存放在任意一端,即通过@JoinColumn注解来映射外键列。一旦选择其中的一端来增加外键,该表即变为从表,另一个表则为主表。
双向1--1关联的主表对应的实体,也不应该用于控制关联关系,因此要用mappedBy属性
Person.java
@Entity
@Table(name="person_info")
public class Person {
@Id
@Column(name="person_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private int age; @OneToOne(targetEntity=Address.class,mappedBy="person")
private Address address;
...
}
Address.java
@Entity
@Table(name="address_info")
public class Address {
@Id
@Column(name="address_id")
@GeneratedValue(strategy=GenerationType.AUTO)
private int addressId;
private String adressDetail;
@OneToOne(targetEntity=Person.class)
@JoinColumn(name="person_id",referencedColumnName="person_id",unique=true)
private Person
}