单向N-1关联 <many-to-one>
单向N-1关系,比如多个人对应同一个住址,只需要从人实体端找到对应的住址实体,无须关系某个地址的全部住户。程序在N的一端增加一个属性,该属性引用1的一端的关联实体。
例如下面person实体中的address属性,
1 package map.six11; 2 3 public class Person { 4 public Integer getId() { 5 return id; 6 } 7 public void setId(Integer id) { 8 this.id = id; 9 } 10 public int getAge() { 11 return age; 12 } 13 public void setAge(int age) { 14 this.age = age; 15 } 16 public String getName() { 17 return name; 18 } 19 public void setName(String name) { 20 this.name = name; 21 } 22 public Address getAddress() { 23 return address; 24 } 25 public void setAddress(Address address) { 26 this.address = address; 27 } 28 29 private Integer id; 30 private int age; 31 private String name; 32 private Address address; 33 34 }
Address是一个独立的实体,
1 package map.six11; 2 3 public class Address { 4 private Integer addressId; 5 private String addressDetail; 6 public Integer getAddressId() { 7 return addressId; 8 } 9 public void setAddressId(Integer addressId) { 10 this.addressId = addressId; 11 } 12 public String getAddressDetail() { 13 return addressDetail; 14 } 15 public void setAddressDetail(String addressDetail) { 16 this.addressDetail = addressDetail; 17 } 18 public Address() {} 19 public Address(String addressDetail) { 20 this.addressDetail = addressDetail; 21 } 22 }
在N的一端person实体的映射文件中,用<many-to-one>标签标识关联的属性实体address。
值得注意的是 cascade="all" 这个属性,当实体类有数据更新时,关联的属性类也会更新到数据库,这叫级联行为。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <many-to-one name="address" cascade="all" class="Address" column="address_id" /> 13 </class> 14 </hibernate-mapping>
在1的一端,则是一个普通的映射文件,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Address" table="address_inf"> 7 <id name="addressId" column="address_id"> 8 <generator class="identity" /> 9 </id> 10 <property name="addressDetail" /> 11 </class> 12 </hibernate-mapping>
下面是一个测试类,
1 package map.six11; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 8 public class PersonManager { 9 public static void testPerson() { 10 Configuration conf = new Configuration().configure(); 11 //conf.addResource("map.six11/Person.hbm.xml"); 12 conf.addClass(Person.class); 13 //conf.addResource("map.six11/Address.hbm.xml"); 14 conf.addClass(Address.class); 15 SessionFactory sf = conf.buildSessionFactory(); 16 Session sess = sf.openSession(); 17 Transaction tx = sess.beginTransaction(); 18 19 Person p = new Person(); 20 Address a = new Address("广州天河"); 21 p.setName("天王盖地虎"); 22 p.setAge(20); 23 p.setAddress(a); 24 sess.persist(p); 25 Address a2 = new Address("上海虹口"); 26 p.setAddress(a2); 27 28 tx.commit(); 29 sess.close(); 30 sf.close(); 31 } 32 33 public static void main(String[] args) { 34 testPerson(); 35 } 36 }
测试类中,Address只是一个瞬态的持久类,从未持久化,但是因为在Person实体类的映射文件中设置了cascade="all"属性,因此Address实体也会随着Person实体的更新而发生级联更新。
因此可以看到Hibernate不仅在Person表中插入了记录,而且还创建了Address表并且也插入了记录,hibernate日志如下,
1 Hibernate: insert into address_inf (addressDetail) values (?) 2 Hibernate: insert into person_inf (age, name, address_id) values (?, ?, ?) 3 Hibernate: insert into address_inf (addressDetail) values (?) 4 Hibernate: update person_inf set age=?, name=?, address_id=? where person_id=?
mysql数据如下,
MariaDB [test]> show tables; +----------------+ | Tables_in_test | +----------------+ | address_inf | | person_inf | +----------------+ 2 rows in set (0.00 sec)
表数据
MariaDB [test]> select * from person_inf; +-----------+------+------------+------------+ | person_id | age | name | address_id | +-----------+------+------------+------------+ | 1 | 20 | 天王盖地虎 | 2 | +-----------+------+------------+------------+ 1 row in set (0.00 sec) MariaDB [test]> select * from address_inf; +------------+---------------+ | address_id | addressDetail | +------------+---------------+ | 1 | 广州天河 | | 2 | 上海虹口 | +------------+---------------+ 2 rows in set (0.00 sec)
表结构如下,person_inf表使用外键address_id与address_inf表关联,形成N-1的关联关系,address_inf表成为了主表。
MariaDB [test]> desc person_inf; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | person_id | int(11) | NO | PRI | NULL | auto_increment | | age | int(11) | YES | | NULL | | | name | varchar(255) | YES | | NULL | | | address_id | int(11) | YES | MUL | NULL | | +------------+--------------+------+-----+---------+----------------+ 4 rows in set (0.01 sec) MariaDB [test]> desc address_inf; +---------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------------+--------------+------+-----+---------+----------------+ | address_id | int(11) | NO | PRI | NULL | auto_increment | | addressDetail | varchar(255) | YES | | NULL | | +---------------+--------------+------+-----+---------+----------------+ 2 rows in set (0.01 sec)
有连接表的N-1关联
连接表
对于上面的person和address两个表的关联有两种实现方式,一种是像上面那样在person表中加入一个外键字段address_id.
第二种方式则是单独用一张连接表来存放person和address的映射关系,例如person_address(person_id,address_id)表,只需要保证person_id 字段不重复,即可实现person和address之间N-1的关联。
如果用Hibernate来实现连接表的N-1关联,只需要修改person实体类的映射文件,用 <join table="person_address"> 来表示连接表,
表中有两个字段,person_id用来关联person表,同时将它作为连接表的主键,用<key>子标签标识,这样能保证person_id在连接表中不会重复。另一个字段addressDetail用来关联address表,同样用<many-to-one>标签来表示这个字段,说明person和address之间的N-1关联关系。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <join table="person_address"> 13 <key column="person_id" /> 14 <many-to-one name="address" cascade="all" class="Address" 15 column="address_id" /> 16 </join> 17 18 </class> 19 </hibernate-mapping>
其他代码不需要做任何修改,执行测试类,发现Hibernate生成了三个表,person_inf和address_inf是两张相对独立的表,person_address则将它们关联起来。
Hibernate: insert into address_inf (addressDetail) values (?) Hibernate: insert into person_inf (age, name) values (?, ?) Hibernate: insert into person_address (address_id, person_id) values (?, ?) Hibernate: insert into address_inf (addressDetail) values (?) Hibernate: update person_address set address_id=? where person_id=?
MariaDB [test]> show tables; +----------------+ | Tables_in_test | +----------------+ | address_inf | | person_address | | person_inf | +----------------+ 3 rows in set (0.00 sec)
表数据,
MariaDB [test]> select * from person_inf; +-----------+------+------------+ | person_id | age | name | +-----------+------+------------+ | 1 | 20 | 天王盖地虎 | +-----------+------+------------+ 1 row in set (0.00 sec) MariaDB [test]> select * from address_inf; +------------+---------------+ | address_id | addressDetail | +------------+---------------+ | 1 | 广州天河 | | 2 | 上海虹口 | +------------+---------------+ 2 rows in set (0.00 sec) MariaDB [test]> select * from person_address; +-----------+------------+ | person_id | address_id | +-----------+------------+ | 1 | 2 | +-----------+------------+ 1 row in set (0.00 sec)
查看person_address表结构,发现person_id成为了主键(即不可重复),
MariaDB [test]> desc person_address; +------------+---------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +------------+---------+------+-----+---------+-------+ | person_id | int(11) | NO | PRI | NULL | | | address_id | int(11) | YES | MUL | NULL | | +------------+---------+------+-----+---------+-------+ 2 rows in set (0.01 sec)
基于外键的单向1-1关联
单向1-1关联与上面的单向N-1关联非常类似,在Hibernate中的实现也非常相似,只需要将上面的单向N-1关联的例子中,在映射文件<many-to-one>标签中假如 unique="true"属性即可,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <many-to-one name="address" cascade="all" class="Address" unique="true" column="address_id" /> 13 </class> 14 </hibernate-mapping>
其他地方不再需要任何修改,执行测试类,会发现得到的表跟之前一模一样,
Hibernate日志
Hibernate: insert into address_inf (addressDetail) values (?) Hibernate: insert into person_inf (age, name, address_id) values (?, ?, ?) Hibernate: insert into address_inf (addressDetail) values (?) Hibernate: update person_inf set age=?, name=?, address_id=? where person_id=?
表数据,
MariaDB [test]> select * from person_inf; +-----------+------+------------+------------+ | person_id | age | name | address_id | +-----------+------+------------+------------+ | 1 | 20 | 天王盖地虎 | 2 | +-----------+------+------------+------------+ 1 row in set (0.00 sec) MariaDB [test]> select * from address_inf; +------------+---------------+ | address_id | addressDetail | +------------+---------------+ | 1 | 广州天河 | | 2 | 上海虹口 | +------------+---------------+ 2 rows in set (0.00 sec)
唯一不同的是,查看 person_inf表结构,发现其外键 address_id多了一个唯一约束,
MariaDB [test]> desc person_inf; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | person_id | int(11) | NO | PRI | NULL | auto_increment | | age | int(11) | YES | | NULL | | | name | varchar(255) | YES | | NULL | | | address_id | int(11) | YES | UNI | NULL | | +------------+--------------+------+-----+---------+----------------+ 4 rows in set (0.01 sec)
有连接表的单向1-1
同样的,只需要像上面那样,在映射文件中加入 unique="true"属性即可,只不过这回是有连接表,用<join>标签而已。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <join table="person_address"> 13 <key column="person_id" /> 14 <many-to-one name="address" cascade="all" class="Address" unique="true" 15 column="address_id" /> 16 </join> 17 </class> 18 </hibernate-mapping>
其他不用做修改,执行测试类,其结果与前面的基于连接表的N-1关联一样,区别是表结构不同,在连接表person_address的addressDetail字段上加了唯一约束,
MariaDB [test]> desc person_address; +------------+---------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +------------+---------+------+-----+---------+-------+ | person_id | int(11) | NO | PRI | NULL | | | address_id | int(11) | YES | UNI | NULL | | +------------+---------+------+-----+---------+-------+ 2 rows in set (0.01 sec)
基于主键的单向1-1 <one-to-one>
前面有基于外键的单向1-1,是在person表中增加外键。而基于主键的单向1-1则是直接让peson表的主键由address表生成,让他们的主键保持一致,使person表成为从表。
这种情况下,在person的映射文件中,需要修改主键生成策略,由原来的identity策略改成foreign策略,并且添name参数来指定关联的实体类,
而关联的address属性,则需要用<one-to-one>来映射,
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="map.six11"> <class name="Person" table="person_inf"> <id name="id" column="person_id" type="int"> <!-- 基于主键关联时,主键生成策略是foreign,表明根据关联类的主键来生成该实体类的主键 --> <generator class="foreign" > <!-- 指定引用关联实体的属性名 --> <param name="property">address</param> </generator> </id> <property name="age" type="int" /> <property name="name" type="string" /> <one-to-one name="address"/> </class> </hibernate-mapping>
其他地方都不需要修改,再次执行测试类,发现person表和address表的主键值是一样的,
MariaDB [test]> select * from address_inf; +------------+---------------+ | address_id | addressDetail | +------------+---------------+ | 1 | 广州天河 | +------------+---------------+ 1 row in set (0.00 sec) MariaDB [test]> select * from person_inf; +-----------+------+------------+ | person_id | age | name | +-----------+------+------------+ | 1 | 20 | 天王盖地虎 | +-----------+------+------------+ 1 row in set (0.00 sec)
无连接表的单向1-N关联
单向1-N关联中,1的一端的实体类需要添加集合属性,而N的一端正是一个集合。
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 public class Person { 7 public Integer getId() { 8 return id; 9 } 10 public void setId(Integer id) { 11 this.id = id; 12 } 13 public int getAge() { 14 return age; 15 } 16 public void setAge(int age) { 17 this.age = age; 18 } 19 public String getName() { 20 return name; 21 } 22 public void setName(String name) { 23 this.name = name; 24 } 25 public Set<Address> getAddresses() { 26 return addresses; 27 } 28 public void setAddresses(Set<Address> addresses) { 29 this.addresses = addresses; 30 } 31 32 33 private Integer id; 34 private int age; 35 private String name; 36 private Set<Address> addresses = new HashSet<>(); 37 38 }
在1的一端实体类的映射文件中,使用<set> <list> <map>等标签来标识关联的集合属性,用子标签<one-to-many>来表示N的一端的实体类。
1的一端与N的一端两个实体类通过1的一端实体类的主键来关联,即在N的一端数据表address_inf中添加外键person_id,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <set name="addresses" table="address_inf"> 13 <key column="person_id"/> 14 <one-to-many class="Address" /> 15 </set> 16 </class> 17 </hibernate-mapping>
这个例子中,因为只需要从1的一端访问N的一端,因此N的一端不需要做改变,实体类Address和映射文件都不需要改变。(当然在底层,hibernate会修改address_inf表,添加一个外键来关联person_inf).
1 package map.six11; 2 3 public class Address { 4 private Integer addressId; 5 private String addressDetail; 6 public Integer getAddressId() { 7 return addressId; 8 } 9 public void setAddressId(Integer addressId) { 10 this.addressId = addressId; 11 } 12 public String getAddressDetail() { 13 return addressDetail; 14 } 15 public void setAddressDetail(String addressDetail) { 16 this.addressDetail = addressDetail; 17 } 18 public Address() {} 19 public Address(String addressDetail) { 20 this.addressDetail = addressDetail; 21 } 22 }
Address实体映射文件,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Address" table="address_inf"> 7 <id name="addressId" column="address_id"> 8 <generator class="identity" /> 9 </id> 10 <property name="addressDetail" /> 11 </class> 12 </hibernate-mapping>
测试类如下,
1 package map.six11; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 8 public class PersonManager { 9 public static void main(String[] args) { 10 Configuration conf = new Configuration().configure(); 11 conf.addClass(Person.class); 12 conf.addClass(Address.class); 13 SessionFactory sf = conf.buildSessionFactory(); 14 Session sess = sf.openSession(); 15 Transaction tx = sess.beginTransaction(); 16 17 Person p = new Person(); 18 Address a = new Address("广州天河"); 19 //需要先持久化Address 对象 20 sess.persist(a); 21 p.setName("天王盖地虎"); 22 p.setAge(21); 23 p.getAddresses().add(a); 24 sess.save(p); 25 26 Address a2 = new Address("上海虹口"); 27 sess.persist(a2); 28 p.getAddresses().add(a2); 29 30 tx.commit(); 31 sess.close(); 32 sf.close(); 33 } 34 }
执行测试类,Hibernate同样生成了person_inf和address_inf两张表,
但是在address_inf表中,还加入了一个外键来关联person_inf表,
表数据如下,可见对于person_inf表来说,person_id是主键,因此具有唯一约束,而它作为address_inf表的外键,可以重复,
因此从person_inf表到address_inf表的映射关系来说,形成了1-N的单向关联,
MariaDB [test]> select * from person_inf; +-----------+------+------------+ | person_id | age | name | +-----------+------+------------+ | 1 | 21 | 天王盖地虎 | +-----------+------+------------+ 1 row in set (0.00 sec) MariaDB [test]> select * from address_inf; +------------+---------------+-----------+ | address_id | addressDetail | person_id | +------------+---------------+-----------+ | 1 | 广州天河 | 1 | | 2 | 上海虹口 | 1 | +------------+---------------+-----------+ 2 rows in set (0.00 sec)
表结构,
MariaDB [test]> desc address_inf; +---------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------------+--------------+------+-----+---------+----------------+ | address_id | int(11) | NO | PRI | NULL | auto_increment | | addressDetail | varchar(255) | YES | | NULL | | | person_id | int(11) | YES | MUL | NULL | | +---------------+--------------+------+-----+---------+----------------+ 3 rows in set (0.01 sec)
上面代码的注解版本,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Column; 8 import javax.persistence.Entity; 9 import javax.persistence.GeneratedValue; 10 import javax.persistence.GenerationType; 11 import javax.persistence.Id; 12 import javax.persistence.JoinColumn; 13 import javax.persistence.OneToMany; 14 import javax.persistence.Table; 15 16 17 @Entity 18 @Table(name="person_inf") 19 public class Person { 20 @Id @Column(name="person_id") 21 @GeneratedValue(strategy=GenerationType.IDENTITY) 22 private Integer id; 23 private int age; 24 private String name; 25 @OneToMany(targetEntity=Address.class) 26 @JoinColumn(name="person_id", referencedColumnName="person_id") 27 private Set<Address> addresses = new HashSet<>(); 28 29 public Integer getId() { 30 return id; 31 } 32 public void setId(Integer id) { 33 this.id = id; 34 } 35 public int getAge() { 36 return age; 37 } 38 public void setAge(int age) { 39 this.age = age; 40 } 41 public String getName() { 42 return name; 43 } 44 public void setName(String name) { 45 this.name = name; 46 } 47 public Set<Address> getAddresses() { 48 return addresses; 49 } 50 public void setAddresses(Set<Address> addresses) { 51 this.addresses = addresses; 52 } 53 }
另外,对于这个单向1-N关联的例子,还有两点需要注意,
1.cascade的用法,可以设置级联更新
在测试类中,我们总是先持久化了Address实体,然后才持久化Person实体,这是为了防止Hibernate报错。
查看Hibernate的日志,我们发现其SQL语句顺序如下,
1 Hibernate: insert into address_inf (addressDetail) values (?) 2 Hibernate: insert into person_inf (age, name) values (?, ?) 3 Hibernate: insert into address_inf (addressDetail) values (?) 4 Hibernate: update address_inf set person_id=? where address_id=? 5 Hibernate: update address_inf set person_id=? where address_id=?
我们发现,Hibernate在插入记录到address_inf表的时候,是要分为两步的,第一步是插入一条person_id为null的记录,第二步是将person表的person_id更新进address_inf表中。
是想如果我们不在测试类中显式地先持久化Address类,当第二步要将person_id更新进address_inf表的时候,根本就找不到对应的记录,那么hibernate就会报错了。
解决这个问题的另一个办法就是设置级联更新,即在person的映射文件中,为address属性添加cascade属性,
1 <set name="addresses" table="address_inf" cascade="all"> 2 <key column="person_id"/> 3 <one-to-many class="Address" /> 4 </set>
如果是通过注解的方式实现的话,则是,
1 @OneToMany(targetEntity=Address.class, cascade=CascadeType.ALL) 2 @JoinColumn(name="person_id", referencedColumnName="person_id") 3 private Set<Address> addresses = new HashSet<>();
这样就能保证即使没有先显示地持久化Address,只要person有更新,所关联的address也会先持久化了。
添加了cascade属性之后hibernate的日志如下,
1 Hibernate: insert into person_inf (age, name) values (?, ?) 2 Hibernate: insert into address_inf (addressDetail) values (?) 3 Hibernate: insert into address_inf (addressDetail) values (?) 4 Hibernate: update address_inf set person_id=? where address_id=? 5 Hibernate: update address_inf set person_id=? where address_id=?
2.单向1-N的性能不高
从上面第1点也可以看出,对于address的持久化,无法通过一条sql语句实现,即在insert into 的时候指定好person_id值,而是需要一条insert和一条update才能实现,这样性能就不高了。
对于这个问题,双向的1-N关联就可以得到解决了,将关联控制转到N的一方,就可以只通过一条SQL语句实现关联实体的持久化。双向关联在后面再总结。
有连接表的单向1-N关联
对于有连接表的单向1-N关联,不再使用<one-to-many>标签,而是使用<many-to-many unique="true">标签,注意要使用unique="true"。不过如果使用JPA 注解的话,依然使用@OneToMany.
当然对于使用连接表实现的1-N关联(集合关联),在映射文件中不再使用<join>标签,而是使用<set>标签。
只不过在无连接表的例子中,<set>直接映射address_inf表,而在这个例子中,只需要将<set>映射单独的关联表person_address即可。
如果使用连接表,则只需要修改Person.hbm.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <set name="addresses" table="person_address"> 13 <key column="person_id"/> 14 <many-to-many class="Address" column="AddressID" unique="true" /> 15 </set> 16 </class> 17 </hibernate-mapping>
执行程序,Hibernate生成了一张person_address表,
表数据,
MariaDB [test]> select * from person_inf; +-----------+------+------------+ | person_id | age | name | +-----------+------+------------+ | 1 | 21 | 天王盖地虎 | +-----------+------+------------+ 1 row in set (0.00 sec) MariaDB [test]> select * from address_inf; +------------+---------------+ | address_id | addressDetail | +------------+---------------+ | 1 | 广州天河 | | 2 | 上海虹口 | +------------+---------------+ 2 rows in set (0.00 sec) MariaDB [test]> select * from person_address; +-----------+-----------+ | person_id | AddressID | +-----------+-----------+ | 1 | 1 | | 1 | 2 | +-----------+-----------+ 2 rows in set (0.00 sec)
表结构
MariaDB [test]> desc person_address; +-----------+---------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-----------+---------+------+-----+---------+-------+ | person_id | int(11) | NO | PRI | NULL | | | AddressID | int(11) | NO | PRI | NULL | | +-----------+---------+------+-----+---------+-------+ 2 rows in set (0.01 sec)
字段addressID唯一约束
MariaDB [test]> SELECT TABLE_NAME,CONSTRAINT_NAME,CONSTRAINT_TYPE FROM informati on_schema.`TABLE_CONSTRAINTS` WHERE TABLE_NAME = 'person_address' ; +----------------+------------------------------+-----------------+ | TABLE_NAME | CONSTRAINT_NAME | CONSTRAINT_TYPE | +----------------+------------------------------+-----------------+ | person_address | PRIMARY | PRIMARY KEY | | person_address | UK_i6yxiouhrtu6lpw50i8n7vbi1 | UNIQUE | | person_address | FK_anrg3ju8wu2kes1a2gr8bp7kg | FOREIGN KEY | | person_address | FK_i6yxiouhrtu6lpw50i8n7vbi1 | FOREIGN KEY | +----------------+------------------------------+-----------------+ 4 rows in set (0.00 sec) MariaDB [test]>
可见对于person_inf表来说,person_id是唯一的,hibernate 将person_id做为了person_address的外键,同时将address_inf的主键address_id也做为了person_address表的外键,但是对person_address表的addressID添加了唯一约束,这就实现了person表和address表在person_address表中1(addressID)-N(person_id)的关联关系.
下面是JPA 注解版,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Column; 8 import javax.persistence.Entity; 9 import javax.persistence.GeneratedValue; 10 import javax.persistence.GenerationType; 11 import javax.persistence.Id; 12 import javax.persistence.JoinColumn; 13 import javax.persistence.JoinTable; 14 import javax.persistence.OneToMany; 15 import javax.persistence.Table; 16 17 18 @Entity 19 @Table(name="person_inf") 20 public class Person { 21 @Id @Column(name="person_id") 22 @GeneratedValue(strategy=GenerationType.IDENTITY) 23 private Integer id; 24 private int age; 25 private String name; 26 @OneToMany(targetEntity = Address.class) 27 @JoinTable(name = "person_address", joinColumns = @JoinColumn(name = "person_id", referencedColumnName = "person_id"), 28 inverseJoinColumns = @JoinColumn(name = "address_id", referencedColumnName = "address_id", unique = true)) 29 private Set<Address> addresses = new HashSet<>(); 30 31 public Integer getId() { 32 return id; 33 } 34 public void setId(Integer id) { 35 this.id = id; 36 } 37 public int getAge() { 38 return age; 39 } 40 public void setAge(int age) { 41 this.age = age; 42 } 43 public String getName() { 44 return name; 45 } 46 public void setName(String name) { 47 this.name = name; 48 } 49 public Set<Address> getAddresses() { 50 return addresses; 51 } 52 public void setAddresses(Set<Address> addresses) { 53 this.addresses = addresses; 54 } 55 }
注意XML映射文件版与JPA注解版的区别,在映射文件中,是通过使用<many-to-many unique="true">标签来映射1-N,而在JPA注解版中,可以直接使用@OneToMany注解了,从语义上来说,注解版更加直观。
单项N-N关联
单向的N-N关联和1-N关联的持久化类完全一样,都是在控制关系的一端增加集合属性(Set),被关联的实体则以集合形式存在。N-N关联必须使用连接表,而且与使用连接表的单向1-N关联非常的相似,只是去掉了unique="true"的限制。映射文件如下,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <set name="addresses" table="person_address"> 13 <key column="person_id"/> 14 <many-to-many class="Address" column="AddressID" /> 15 </set> 16 </class> 17 </hibernate-mapping>
执行结果与1-N关联一模一样,只不过生成的person_address表的addressID字段不再有唯一约束。
无连接表的双向1-N关联
双向1-N关联不仅对于实体类需要增加管理属性(集合),在关联实体中也需要增加外键,因此实体类和关联类都需要修改。同时,Hibernate建议不要在1的一端控制关系,因此要在1的一端的映射文件加inverse="true"属性(JPA注解版则是加mappedBy)属性。
Person实体类,注意加粗的关联集合,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 7 public class Person { 8 private Integer id; 9 private int age; 10 private String name; 11 private Set<Address> addresses = new HashSet<>(); 12 13 public Integer getId() { 14 return id; 15 } 16 public void setId(Integer id) { 17 this.id = id; 18 } 19 public int getAge() { 20 return age; 21 } 22 public void setAge(int age) { 23 this.age = age; 24 } 25 public String getName() { 26 return name; 27 } 28 public void setName(String name) { 29 this.name = name; 30 } 31 public Set<Address> getAddresses() { 32 return addresses; 33 } 34 public void setAddresses(Set<Address> addresses) { 35 this.addresses = addresses; 36 } 37 }
映射文件,指定关联实体类,及关联的外键person_id,同时设置inverse=true属性表明1的一端不再控制关系。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <set name="addresses" inverse="true"> 13 <key column="person_id"/> 14 <one-to-many class="Address" /> 15 </set> 16 </class> 17 </hibernate-mapping>
关联的实体类Address, 注意加粗的实体类引用,
1 package map.six11; 2 3 public class Address { 4 private Integer addressId; 5 private String addressDetail; 6 private Person person; 7 public Integer getAddressId() { 8 return addressId; 9 } 10 public void setAddressId(Integer addressId) { 11 this.addressId = addressId; 12 } 13 public String getAddressDetail() { 14 return addressDetail; 15 } 16 public void setAddressDetail(String addressDetail) { 17 this.addressDetail = addressDetail; 18 } 19 public Address() {} 20 public Address(String addressDetail) { 21 this.addressDetail = addressDetail; 22 } 23 public Person getPerson() { 24 return person; 25 } 26 public void setPerson(Person person) { 27 this.person = person; 28 } 29 }
Address的映射文件,通过many-to-one>关联引用实体类,也需要指定外键,并且必须和Person指定的是同一个外键。
设置not-null可以保证每条从表记录(address_inf)都有与之对应的主表(person_inf)记录。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Address" table="address_inf"> 7 <id name="addressId" column="address_id"> 8 <generator class="identity" /> 9 </id> 10 <property name="addressDetail" /> 11 <!-- 双向1-N关联的关联实体中,必须指定person_id且与实体类的Set属性中的key column相同 --> 12 <many-to-one name="person" class="Person" column="person_id" not-null="true" /> 13 </class> 14 </hibernate-mapping>
测试类如下,注意总是先持久化实体类,再设置关联类与持久类关系,最后持久化关联类,
1 package map.six11; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 8 public class PersonManager { 9 public static void main(String[] args) { 10 Configuration conf = new Configuration().configure(); 11 conf.addClass(Person.class); 12 conf.addClass(Address.class); 13 //conf.addAnnotatedClass(Person.class); 14 //conf.addAnnotatedClass(Address.class); 15 SessionFactory sf = conf.buildSessionFactory(); 16 Session sess = sf.openSession(); 17 Transaction tx = sess.beginTransaction(); 18 19 //先持久化主表 20 Person p = new Person(); 21 p.setName("天王盖地虎"); 22 p.setAge(21); 23 sess.save(p); 24 25 Address a = new Address("广州天河"); 26 //先设置关联,再持久化a 27 a.setPerson(p); 28 sess.persist(a); 29 30 Address a2 = new Address("上海虹口"); 31 //先设置关联,再持久化a 32 a2.setPerson(p); 33 sess.persist(a2); 34 35 tx.commit(); 36 sess.close(); 37 sf.close(); 38 } 39 }
执行测试类,发现Hibernate一共只执行了3条SQL语句完成持久化,
Hibernate: insert into person_inf (age, name) values (?, ?) Hibernate: insert into adddress_inf (addressDetail, person_id) values (?, ?) Hibernate: insert into adddress_inf (addressDetail, person_id) values (?, ?)
在mysql中,address_inf表增加了一个person_id外键字段,
MariaDB [test]> select * from person_inf; +-----------+-----+------------+ | person_id | age | name | +-----------+-----+------------+ | 1 | 21 | 天王盖地虎 | +-----------+-----+------------+ 1 row in set (0.00 sec) MariaDB [test]> select * from address_inf; +------------+---------------+-----------+ | address_id | addressDetail | person_id | +------------+---------------+-----------+ | 1 | 广州天河 | 1 | | 2 | 上海虹口 | 1 | +------------+---------------+-----------+ 2 rows in set (0.00 sec) MariaDB [test]> desc address_inf; +---------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------------+--------------+------+-----+---------+----------------+ | address_id | int(11) | NO | PRI | NULL | auto_increment | | addressDetail | varchar(255) | YES | | NULL | | | person_id | int(11) | NO | MUL | NULL | | +---------------+--------------+------+-----+---------+----------------+ 3 rows in set (0.01 sec)
下面是JPA注解版,
Person为addressess集合属性添加@OneToMany属性并设置mappedBy="person"表明不再控制关系,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Column; 8 import javax.persistence.Entity; 9 import javax.persistence.GeneratedValue; 10 import javax.persistence.GenerationType; 11 import javax.persistence.Id; 12 import javax.persistence.JoinColumn; 13 import javax.persistence.JoinTable; 14 import javax.persistence.OneToMany; 15 import javax.persistence.Table; 16 17 18 @Entity 19 @Table(name="person_inf") 20 public class Person { 21 @Id @Column(name="person_id") 22 @GeneratedValue(strategy=GenerationType.IDENTITY) 23 private Integer id; 24 private int age; 25 private String name; 26 @OneToMany(targetEntity=Address.class, mappedBy="person") 27 private Set<Address> addresses = new HashSet<>(); 28 29 public Integer getId() { 30 return id; 31 } 32 public void setId(Integer id) { 33 this.id = id; 34 } 35 public int getAge() { 36 return age; 37 } 38 public void setAge(int age) { 39 this.age = age; 40 } 41 public String getName() { 42 return name; 43 } 44 public void setName(String name) { 45 this.name = name; 46 } 47 public Set<Address> getAddresses() { 48 return addresses; 49 } 50 public void setAddresses(Set<Address> addresses) { 51 this.addresses = addresses; 52 } 53 }
关联实体类,为关联实体person设置@ManyToOne注解并设置person_id为外键。
1 package map.six11; 2 3 import javax.persistence.Column; 4 import javax.persistence.Entity; 5 import javax.persistence.GeneratedValue; 6 import javax.persistence.GenerationType; 7 import javax.persistence.Id; 8 import javax.persistence.JoinColumn; 9 import javax.persistence.ManyToOne; 10 import javax.persistence.Table; 11 12 @Entity 13 @Table(name="address_inf") 14 public class Address { 15 @Id @Column(name="address_id") 16 @GeneratedValue(strategy=GenerationType.IDENTITY) 17 private Integer addressId; 18 private String addressDetail; 19 @ManyToOne(targetEntity=Person.class) 20 @JoinColumn(name="person_id", referencedColumnName="person_id", nullable=false) 21 private Person person; 22 public Integer getAddressId() { 23 return addressId; 24 } 25 public void setAddressId(Integer addressId) { 26 this.addressId = addressId; 27 } 28 public String getAddressDetail() { 29 return addressDetail; 30 } 31 public void setAddressDetail(String addressDetail) { 32 this.addressDetail = addressDetail; 33 } 34 public Address() {} 35 public Address(String addressDetail) { 36 this.addressDetail = addressDetail; 37 } 38 public Person getPerson() { 39 return person; 40 } 41 public void setPerson(Person person) { 42 this.person = person; 43 } 44 }
对于双向1-N关联,需要注意以下几点
1.对于1-N关联,Hibernate推荐使用双向1-N关联,并且不要让1的一端控制关联关系。(通过设置inverse=true或mappedBy实现)
2.出于性能考虑,应该先持久化1的一端实体类,这样在insert N的一端数据时候,就可以直接指定外键的值了,否则就需要update才能实现,多了一条SQL
3.先设置关联关系(本例中的a.setPerson(person)), 再保存关联对象,也是处于性能提升的考量。
双向有连接表的1-N关联
双向1-N关联也可以指定连接表,表中有两个字段,分别是实体类与关联类的主键即可。在映射文件中,1和N两端都添加关联到连接表。
在1的一端,与单向有连接表的1-N关联非常类似,但又不是完全相同。 在单向1-N有连接表关联时,我们可以用<set>标签关联连接表,用<key>关联本类主键到连接表作为外键,指定<many-to-many class="Address" column="AddressID" unique="true" />来关联集合属性,这里的column名称将会是关联表里的字段名称,我们可以定义为任意名字;然而在双向1-N有连接表关联中,这里的column名字不能随意指定,必须跟关联实体类中,对应的字段名称一致,关联实体中的字段名字叫address_id,因而这里(person映射文件及关联表字段)名称也必须是address_id。
而在N的一端,需要添加<join>标签强制关联连接表,也用<key>关联到本类主键连接表作为外键,指定<many-to-one name="person", column="person_id", not-null="true" />关联实体类,这里的column也不能随意命名,必须是person表中对应的字段名称。映射文件关键部分代码如下,
person映射文件,
1 <set name="addresses" inverse="true" table="person_address"> 2 <key column="person_id" /> 3 <many-to-many class="Address" column="address_id" unique="true" /> 4 </set>
Address映射文件,
1 <join table="person_address"> 2 <key column="address_id" /> 3 <many-to-one name="person" column="person_id" not-null="true" /> 4 </join>
完整代码如下,
person实体类,添加集合属性 addresses,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 7 public class Person { 8 private Integer id; 9 private int age; 10 private String name; 11 private Set<Address> addresses = new HashSet<>(); 12 13 public Integer getId() { 14 return id; 15 } 16 public void setId(Integer id) { 17 this.id = id; 18 } 19 public int getAge() { 20 return age; 21 } 22 public void setAge(int age) { 23 this.age = age; 24 } 25 public String getName() { 26 return name; 27 } 28 public void setName(String name) { 29 this.name = name; 30 } 31 public Set<Address> getAddresses() { 32 return addresses; 33 } 34 public void setAddresses(Set<Address> addresses) { 35 this.addresses = addresses; 36 } 37 }
映射文件,指定连接表和关联类及关联字段
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <set name="addresses" inverse="true" table="person_address"> 13 <key column="person_id" /> 14 <many-to-many class="Address" column="address_id" unique="true" /> 15 </set> 16 </class> 17 </hibernate-mapping>
Address实体类,添加Person实体类的引用,
1 package map.six11; 2 3 public class Address { 4 private Integer addressId; 5 private String addressDetail; 6 private Person person; 7 public Integer getAddressId() { 8 return addressId; 9 } 10 public void setAddressId(Integer addressId) { 11 this.addressId = addressId; 12 } 13 public String getAddressDetail() { 14 return addressDetail; 15 } 16 public void setAddressDetail(String addressDetail) { 17 this.addressDetail = addressDetail; 18 } 19 public Address() {} 20 public Address(String addressDetail) { 21 this.addressDetail = addressDetail; 22 } 23 public Person getPerson() { 24 return person; 25 } 26 public void setPerson(Person person) { 27 this.person = person; 28 } 29 }
映射文件,指定连接表和关联类及关联字段,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Address" table="address_inf"> 7 <id name="addressId" column="address_id"> 8 <generator class="identity" /> 9 </id> 10 <property name="addressDetail" /> 11 <join table="person_address"> 12 <key column="address_id" /> 13 <many-to-one name="person" column="person_id" not-null="true" /> 14 </join> 15 </class> 16 </hibernate-mapping>
测试类,
1 package map.six11; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 8 public class PersonManager { 9 public static void main(String[] args) { 10 Configuration conf = new Configuration().configure(); 11 conf.addClass(Person.class); 12 conf.addClass(Address.class); 13 //conf.addAnnotatedClass(Person.class); 14 //conf.addAnnotatedClass(Address.class); 15 SessionFactory sf = conf.buildSessionFactory(); 16 Session sess = sf.openSession(); 17 Transaction tx = sess.beginTransaction(); 18 19 //先持久化主表 20 Person p = new Person(); 21 p.setName("天王盖地虎"); 22 p.setAge(21); 23 sess.save(p); 24 25 Address a = new Address("广州天河"); 26 //先设置关联,再持久化a 27 a.setPerson(p); 28 sess.persist(a); 29 30 Address a2 = new Address("上海虹口"); 31 //先设置关联,再持久化a 32 a2.setPerson(p); 33 sess.persist(a2); 34 35 tx.commit(); 36 sess.close(); 37 sf.close(); 38 } 39 }
执行测试类,Hibernate生成3张表,在person_address表上有两个外键约束,分别关联person_inf和address_inf的主键,Hibernate为上面每一个持久化生成一个insert语句,加上为连接表插入数据的语句,一共有5条SQL语句,
Hibernate: insert into person_inf (age, name) values (?, ?) Hibernate: insert into address_inf (addressDetail) values (?) Hibernate: insert into person_address (person_id, address_id) values (?, ?) Hibernate: insert into address_inf (addressDetail) values (?) Hibernate: insert into person_address (person_id, address_id) values (?, ?)
表数据和结构如下,
MariaDB [information_schema]> SELECT * FROM `TABLE_CONSTRAINTS` WHERE `TABLE_NAME` = 'person_address' ; +--------------------+-------------------+------------------------------+--------------+----------------+-----------------+ | CONSTRAINT_CATALOG | CONSTRAINT_SCHEMA | CONSTRAINT_NAME | TABLE_SCHEMA | TABLE_NAME | CONSTRAINT_TYPE | +--------------------+-------------------+------------------------------+--------------+----------------+-----------------+ | def | test | PRIMARY | test | person_address | PRIMARY KEY | | def | test | FK_anrg3ju8wu2kes1a2gr8bp7kg | test | person_address | FOREIGN KEY | | def | test | FK_d0akgh2385j4j0w78l54f6lkg | test | person_address | FOREIGN KEY | +--------------------+-------------------+------------------------------+--------------+----------------+-----------------+ 3 rows in set (0.00 sec) MariaDB [information_schema]> use test; Database changed MariaDB [test]> select * from person_address; +------------+-----------+ | address_id | person_id | +------------+-----------+ | 1 | 1 | | 2 | 1 | +------------+-----------+ 2 rows in set (0.00 sec)
下面是JPA注解版
相比起来,注解版就简单多了,Person实体类与不带连接表的双向1-N关联中的实体类一模一样不需要改变,仅仅是在Address实体类中加入@ManyToOne和@JoinTable,
Address实体类,
1 package map.six11; 2 3 import javax.persistence.Column; 4 import javax.persistence.Entity; 5 import javax.persistence.GeneratedValue; 6 import javax.persistence.GenerationType; 7 import javax.persistence.Id; 8 import javax.persistence.JoinTable; 9 import javax.persistence.JoinColumn; 10 import javax.persistence.ManyToOne; 11 import javax.persistence.Table; 12 13 @Entity 14 @Table(name="address_inf") 15 public class Address { 16 @Id @Column(name="address_id") 17 @GeneratedValue(strategy=GenerationType.IDENTITY) 18 private Integer addressId; 19 private String addressDetail; 20 @ManyToOne(targetEntity=Person.class) 21 @JoinTable(name="person_address", 22 joinColumns=@JoinColumn(name="address_id", referencedColumnName="address_id", unique=true), 23 inverseJoinColumns=@JoinColumn(name="person_id", referencedColumnName="person_id") 24 ) 25 private Person person; 26 public Integer getAddressId() { 27 return addressId; 28 } 29 public void setAddressId(Integer addressId) { 30 this.addressId = addressId; 31 } 32 public String getAddressDetail() { 33 return addressDetail; 34 } 35 public void setAddressDetail(String addressDetail) { 36 this.addressDetail = addressDetail; 37 } 38 public Address() {} 39 public Address(String addressDetail) { 40 this.addressDetail = addressDetail; 41 } 42 public Person getPerson() { 43 return person; 44 } 45 public void setPerson(Person person) { 46 this.person = person; 47 } 48 }
执行结果与前面一样。
双向N-N关联
双向N-N关联只能用连接表实现。在关联实体两边都添加Set集合属性,在映射文件中都使用<Set>集合标签关联连接表,都使用<key>标签标识连接表外键(实体类主键),都使用<many-to-many>表示被关联实体类并指明关联的字段,都不能使用unique=true属性因为是多对多关联。
Person实体类映射文件,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <set name="addresses" inverse="true" table="person_address"> 13 <key column="person_id" /> 14 <many-to-many class="Address" column="address_id" /> 15 </set> 16 </class> 17 </hibernate-mapping>
Address实体类映射文件,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Address" table="address_inf"> 7 <id name="addressId" column="address_id"> 8 <generator class="identity" /> 9 </id> 10 <property name="addressDetail" /> 11 <join table="person_address"> 12 <key column="address_id" /> 13 <many-to-one name="person" column="person_id" not-null="true" /> 14 </join> 15 </class> 16 </hibernate-mapping>
下面是JPA注解版,
Person实体,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.Column; 7 import javax.persistence.Entity; 8 import javax.persistence.GeneratedValue; 9 import javax.persistence.GenerationType; 10 import javax.persistence.Id; 11 import javax.persistence.JoinTable; 12 import javax.persistence.JoinColumn; 13 import javax.persistence.ManyToMany; 14 import javax.persistence.OneToMany; 15 import javax.persistence.Table; 16 17 @Entity 18 @Table(name="person_inf") 19 public class Person { 20 @Id @Column(name="person_id") 21 @GeneratedValue(strategy=GenerationType.IDENTITY) 22 private Integer id; 23 private int age; 24 private String name; 25 @ManyToMany(targetEntity=Address.class) 26 @JoinTable(name="person_address", 27 joinColumns=@JoinColumn(name="person_id", referencedColumnName="person_id"), 28 inverseJoinColumns=@JoinColumn(name="address_id", referencedColumnName="address_id")) 29 private Set<Address> addresses = new HashSet<>(); 30 31 public Integer getId() { 32 return id; 33 } 34 public void setId(Integer id) { 35 this.id = id; 36 } 37 public int getAge() { 38 return age; 39 } 40 public void setAge(int age) { 41 this.age = age; 42 } 43 public String getName() { 44 return name; 45 } 46 public void setName(String name) { 47 this.name = name; 48 } 49 public Set<Address> getAddresses() { 50 return addresses; 51 } 52 public void setAddresses(Set<Address> addresses) { 53 this.addresses = addresses; 54 } 55 }
Address实体,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.Column; 7 import javax.persistence.Entity; 8 import javax.persistence.GeneratedValue; 9 import javax.persistence.GenerationType; 10 import javax.persistence.Id; 11 import javax.persistence.JoinTable; 12 import javax.persistence.JoinColumn; 13 import javax.persistence.ManyToMany; 14 import javax.persistence.ManyToOne; 15 import javax.persistence.Table; 16 17 @Entity 18 @Table(name="address_inf") 19 public class Address { 20 @Id @Column(name="address_id") 21 @GeneratedValue(strategy=GenerationType.IDENTITY) 22 private Integer addressId; 23 private String addressDetail; 24 @ManyToMany(targetEntity=Person.class) 25 @JoinTable(name="person_address", 26 joinColumns=@JoinColumn(name="address_id", referencedColumnName="address_id"), 27 inverseJoinColumns=@JoinColumn(name="person_id", referencedColumnName="person_id") 28 ) 29 private Set<Person> person = new HashSet<>(); 30 public Set<Person> getPerson() { 31 return person; 32 } 33 public void setPerson(Set<Person> person) { 34 this.person = person; 35 } 36 public Integer getAddressId() { 37 return addressId; 38 } 39 public void setAddressId(Integer addressId) { 40 this.addressId = addressId; 41 } 42 public String getAddressDetail() { 43 return addressDetail; 44 } 45 public void setAddressDetail(String addressDetail) { 46 this.addressDetail = addressDetail; 47 } 48 public Address() {} 49 public Address(String addressDetail) { 50 this.addressDetail = addressDetail; 51 } 52 }
双向1-1关联 (基于外键)
通过在表中添加外键列实现1-1关联,外键可以添加在任意一边表中,映射文件则添加<many-to-one unique="true">标签来关联另一个实体,未添加外键的实体映射文件则用<one-to-one>来关联实体,关键代码如下。
现在假如将外键列添加在address_inf表中,
则Person映射文件为,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <one-to-one name="address" property-ref="person"/> 13 </class> 14 </hibernate-mapping>
Address映射文件为,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Address" table="address_inf"> 7 <id name="addressId" column="address_id"> 8 <generator class="identity" /> 9 </id> 10 <property name="addressDetail" /> 11 <many-to-one name="person" unique="true" column="person_id" not-null="true" /> 12 </class> 13 </hibernate-mapping>
如果用JPA注解实现,则Person实体类为,
1 package map.six11; 2 3 import javax.persistence.Column; 4 import javax.persistence.Entity; 5 import javax.persistence.GeneratedValue; 6 import javax.persistence.GenerationType; 7 import javax.persistence.Id; 8 import javax.persistence.OneToOne; 9 import javax.persistence.Table; 10 11 12 @Entity 13 @Table(name="person_inf") 14 public class Person { 15 @Id @Column(name="person_id") 16 @GeneratedValue(strategy=GenerationType.IDENTITY) 17 private Integer id; 18 private int age; 19 private String name; 20 @OneToOne(targetEntity=Address.class, mappedBy="person") 21 private Address address; 22 23 public Integer getId() { 24 return id; 25 } 26 public void setId(Integer id) { 27 this.id = id; 28 } 29 public int getAge() { 30 return age; 31 } 32 public void setAge(int age) { 33 this.age = age; 34 } 35 public String getName() { 36 return name; 37 } 38 public void setName(String name) { 39 this.name = name; 40 } 41 public Address getAddress() { 42 return address; 43 } 44 public void setAddress(Address address) { 45 this.address = address; 46 } 47 }
Address实体类如下,两边实体类都使用的是@OneToOne, 这更符合语意。 在Address这边,显示地指明了关联外键列。
1 package map.six11; 2 3 4 import javax.persistence.Column; 5 import javax.persistence.Entity; 6 import javax.persistence.GeneratedValue; 7 import javax.persistence.GenerationType; 8 import javax.persistence.Id; 9 import javax.persistence.JoinColumn; 10 import javax.persistence.ManyToOne; 11 import javax.persistence.OneToOne; 12 import javax.persistence.Table; 13 14 15 @Entity 16 @Table(name="address_inf") 17 public class Address { 18 @Id @Column(name="address_id") 19 @GeneratedValue(strategy=GenerationType.IDENTITY) 20 private Integer addressId; 21 private String addressDetail; 22 @OneToOne(targetEntity=Person.class) 23 @JoinColumn(name="person_id", referencedColumnName="person_id", unique=true) 24 private Person person; 25 26 public Integer getAddressId() { 27 return addressId; 28 } 29 public void setAddressId(Integer addressId) { 30 this.addressId = addressId; 31 } 32 public String getAddressDetail() { 33 return addressDetail; 34 } 35 public void setAddressDetail(String addressDetail) { 36 this.addressDetail = addressDetail; 37 } 38 public Address() {} 39 public Address(String addressDetail) { 40 this.addressDetail = addressDetail; 41 } 42 public Person getPerson() { 43 return person; 44 } 45 public void setPerson(Person person) { 46 this.person = person; 47 } 48 }
双向1-1关联 (基于主键)
基于主键的双向1-1关联,两边都使用<one-to-one>关联另对方实体类,只不过需要在其中一个实体类中,将主键生成器设置为foreign,使其变成从表,
比如在Address映射文件中,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Address" table="address_inf"> 7 <id name="addressId" column="address_id"> 8 <generator class="foreign" > 9 <param name="property">person</param> 10 </generator> 11 </id> 12 <property name="addressDetail" /> 13 <one-to-one name="person" /> 14 </class> 15 </hibernate-mapping>
在Person映射文件中,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <one-to-one name="address" /> 13 </class> 14 </hibernate-mapping>
双向1-1关联 (基于连接表)
Hibernate并不推荐基于连接表的双向1-1关联,因为映射比较复杂。
在实体两边,都使用<join>标签指定连接表,都使用<many-to-one>映射关联类。
Person映射文件如下,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <join table="person_address" inverse="true"> 13 <key column="person_id" unique="true" /> 14 <many-to-one name="address" class="Address" column="address_id" unique="true" /> 15 </join> 16 </class> 17 </hibernate-mapping>
Address映射文件如下,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Address" table="address_inf"> 7 <id name="addressId" column="address_id"> 8 <generator class="identity" /> 9 </id> 10 <property name="addressDetail" /> 11 <join table="person_address" optional="true"> 12 <key column="person_id" unique="true" /> 13 <many-to-one name="person" class="Person" column="person_id" 14 unique="true" /> 15 </join> 16 </class> 17 </hibernate-mapping>
两个映射文件的区别是,Address的address_id将作为连接表的主键,因此Address映射文件为<join 指定optional=true属性,而Person则为<join指定inverse=true属性。
下面是JPA注解实现,两边都是用@OneToOne注解映射关联类,两边都使用@JoinTable表示连接表,两个实体类写法都是一样的,只是互相引用对方而已,
Person实体类,
1 package map.six11; 2 3 import javax.persistence.Column; 4 import javax.persistence.Entity; 5 import javax.persistence.GeneratedValue; 6 import javax.persistence.GenerationType; 7 import javax.persistence.Id; 8 import javax.persistence.JoinTable; 9 import javax.persistence.JoinColumn; 10 import javax.persistence.OneToOne; 11 import javax.persistence.Table; 12 13 14 @Entity 15 @Table(name="person_inf") 16 public class Person { 17 @Id @Column(name="person_id") 18 @GeneratedValue(strategy=GenerationType.IDENTITY) 19 private Integer id; 20 private int age; 21 private String name; 22 @OneToOne(targetEntity=Address.class) 23 @JoinTable(name="person_address", 24 joinColumns=@JoinColumn(name="person_id", referencedColumnName="person_id", unique=true), 25 inverseJoinColumns=@JoinColumn(name="access_id", referencedColumnName="address_id", unique=true) 26 ) 27 private Address address; 28 29 public Integer getId() { 30 return id; 31 } 32 public void setId(Integer id) { 33 this.id = id; 34 } 35 public int getAge() { 36 return age; 37 } 38 public void setAge(int age) { 39 this.age = age; 40 } 41 public String getName() { 42 return name; 43 } 44 public void setName(String name) { 45 this.name = name; 46 } 47 public Address getAddress() { 48 return address; 49 } 50 public void setAddress(Address address) { 51 this.address = address; 52 } 53 }
Address实体类,
1 package map.six11; 2 3 4 import javax.persistence.Column; 5 import javax.persistence.Entity; 6 import javax.persistence.GeneratedValue; 7 import javax.persistence.GenerationType; 8 import javax.persistence.Id; 9 import javax.persistence.JoinColumn; 10 import javax.persistence.JoinTable; 11 import javax.persistence.ManyToOne; 12 import javax.persistence.OneToOne; 13 import javax.persistence.Table; 14 15 16 @Entity 17 @Table(name="address_inf") 18 public class Address { 19 @Id @Column(name="address_id") 20 @GeneratedValue(strategy=GenerationType.IDENTITY) 21 private Integer addressId; 22 private String addressDetail; 23 @OneToOne(targetEntity=Person.class) 24 @JoinTable(name="person_address", 25 joinColumns=@JoinColumn(name="address_id", referencedColumnName="address_id", unique=true), 26 inverseJoinColumns=@JoinColumn(name="person_id", referencedColumnName="person_id", unique=true) 27 ) 28 private Person person; 29 30 public Integer getAddressId() { 31 return addressId; 32 } 33 public void setAddressId(Integer addressId) { 34 this.addressId = addressId; 35 } 36 public String getAddressDetail() { 37 return addressDetail; 38 } 39 public void setAddressDetail(String addressDetail) { 40 this.addressDetail = addressDetail; 41 } 42 public Address() {} 43 public Address(String addressDetail) { 44 this.addressDetail = addressDetail; 45 } 46 public Person getPerson() { 47 return person; 48 } 49 public void setPerson(Person person) { 50 this.person = person; 51 } 52 }
组件属性包含关联实体
如果一个持久化类Person的一个组件属性Address中,又有一个属性School也是一个持久化类,那么从逻辑上来讲,Address和School应该是关联的,但是如果Address仅仅是一个组件而不是实体类,那么就会变成Person和School关联。
比如下面这样,Person类中有一个组件属性Address,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 7 public class Person { 8 private Integer id; 9 private int age; 10 private String name; 11 private Address address; 12 13 public Integer getId() { 14 return id; 15 } 16 public void setId(Integer id) { 17 this.id = id; 18 } 19 public int getAge() { 20 return age; 21 } 22 public void setAge(int age) { 23 this.age = age; 24 } 25 public String getName() { 26 return name; 27 } 28 public void setName(String name) { 29 this.name = name; 30 } 31 public Address getAddress() { 32 return address; 33 } 34 public void setAddress(Address address) { 35 this.address = address; 36 } 37 }
组件Address(注意这里只当作组件用,不再当作持久化了类, 因此也不会有映射文件)
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 public class Address { 7 private Integer addressId; 8 private String addressDetail; 9 private Person person; 10 private Set<School> schools = new HashSet<>(); 11 public Integer getAddressId() { 12 return addressId; 13 } 14 public void setAddressId(Integer addressId) { 15 this.addressId = addressId; 16 } 17 public String getAddressDetail() { 18 return addressDetail; 19 } 20 public void setAddressDetail(String addressDetail) { 21 this.addressDetail = addressDetail; 22 } 23 public Address() {} 24 public Address(String addressDetail) { 25 this.addressDetail = addressDetail; 26 } 27 public Person getPerson() { 28 return person; 29 } 30 public void setPerson(Person person) { 31 this.person = person; 32 } 33 public Set<School> getSchools() { 34 return schools; 35 } 36 public void setSchools(Set<School> schools) { 37 this.schools = schools; 38 } 39 }
在组件属性Address中包含了一个集合属性school, shool又是一个持久化类,代码如下,
1 package map.six11; 2 3 public class School { 4 private Integer schoolId; 5 private String schoolName; 6 7 public String getSchoolName() { 8 return schoolName; 9 } 10 11 public void setSchoolName(String schoolName) { 12 this.schoolName = schoolName; 13 } 14 15 public School(){} 16 17 public School(String schoolName) { 18 this.schoolName = schoolName; 19 } 20 21 public Integer getSchoolId() { 22 return schoolId; 23 } 24 25 public void setSchoolId(Integer schoolId) { 26 this.schoolId = schoolId; 27 } 28 }
可见,从逻辑上来说,Address与School形成1-N的单向关联。
Person映射文件如下,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="Person" table="person_inf"> 7 <id name="id" column="person_id" type="int"> 8 <generator class="identity" /> 9 </id> 10 <property name="age" type="int" /> 11 <property name="name" type="string" /> 12 <!-- 映射主键元素 --> 13 <component name="address" class="Address"> 14 <!-- 映射组件的包含实体 --> 15 <parent name="person" /> 16 <property name="addressDetail" /> 17 <!-- 映射组件类的集合属性, 集合元素又是其他持久化实体类 --> 18 <set name="schools"> 19 <!-- 外键 --> 20 <key column="address_id" /> 21 <!-- 1-N关联 --> 22 <one-to-many class="School" /> 23 </set> 24 </component> 25 </class> 26 </hibernate-mapping>
Address不是持久化类,没有映射文件。 School映射文件如下,
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 4 5 <hibernate-mapping package="map.six11"> 6 <class name="School" table="school_inf"> 7 <id name="schoolId" column="school_id"> 8 <generator class="identity" /> 9 </id> 10 <property name="schoolName" /> 11 </class> 12 </hibernate-mapping>
测试类如下,
1 package map.six11; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 8 public class PersonManager { 9 public static void main(String[] args) { 10 Configuration conf = new Configuration().configure(); 11 conf.addClass(Person.class); 12 conf.addClass(School.class); 13 //conf.addClass(Address.class); 14 //conf.addAnnotatedClass(Person.class); 15 //conf.addAnnotatedClass(Address.class); 16 SessionFactory sf = conf.buildSessionFactory(); 17 Session sess = sf.openSession(); 18 Transaction tx = sess.beginTransaction(); 19 20 //先持久化主表 21 Person p = new Person(); 22 p.setName("天王盖地虎"); 23 p.setAge(21); 24 sess.save(p); 25 26 Address a = new Address("广州天河"); 27 p.setAddress(a); 28 29 School s1 = new School("北京大学"); 30 School s2 = new School("清华大学"); 31 32 sess.save(s1); 33 sess.save(s2); 34 35 a.getSchools().add(s1); 36 a.getSchools().add(s2); 37 38 tx.commit(); 39 sess.close(); 40 sf.close(); 41 } 42 }
测试类中,我们只需要加载Person和School两个类作为持久化类。
程序必须先持久化两个School对象,因为School对象没有对Address的引用,而映射文件要求有一个address_id外键,因此这必然导致insert之后的update。
执行测试类,Hibernate生成的SQL如下,
Hibernate: insert into person_inf (age, name, addressDetail) values (?, ?, ?) Hibernate: insert into school_inf (schoolName) values (?) Hibernate: insert into school_inf (schoolName) values (?) Hibernate: update person_inf set age=?, name=?, addressDetail=? where person_id=? Hibernate: update school_inf set address_id=? where school_id=? Hibernate: update school_inf set address_id=? where school_id=?
查看数据库的数据,发现生成两张表,person_inf和school_inf, person_inf中有address信息,person_inf中则有address_inf外键。
从逻辑上来讲,address得是一个持久化类,并和school形成1-N单向关联,然而这里的address根本不是一个持久化类,这导致了school_inf表与person_inf表关联到了一起,看起来很混乱。
事实上,Hibernate也不推荐 组件里包含关联实体 , 因为这确实没啥意义,不如直接将组件也持久化。
表信息,
MariaDB [test]> select * from person_inf; +-----------+------+------------+---------------+ | person_id | age | name | addressDetail | +-----------+------+------------+---------------+ | 1 | 21 | 天王盖地虎 | 广州天河 | +-----------+------+------------+---------------+ 1 row in set (0.00 sec) MariaDB [test]> select * from school_inf; +-----------+------------+------------+ | school_id | schoolName | address_id | +-----------+------------+------------+ | 1 | 北京大学 | 1 | | 2 | 清华大学 | 1 | +-----------+------------+------------+ 2 rows in set (0.00 sec)
表约束,
下面附上JPA注解版,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Column; 8 import javax.persistence.Entity; 9 import javax.persistence.GeneratedValue; 10 import javax.persistence.GenerationType; 11 import javax.persistence.Id; 12 import javax.persistence.JoinColumn; 13 import javax.persistence.JoinTable; 14 import javax.persistence.OneToMany; 15 import javax.persistence.Table; 16 17 18 @Entity 19 @Table(name="person_inf") 20 public class Person { 21 @Id @Column(name="person_id") 22 @GeneratedValue(strategy=GenerationType.IDENTITY) 23 private Integer id; 24 private int age; 25 private String name; 26 private Address address; 27 28 public Integer getId() { 29 return id; 30 } 31 public void setId(Integer id) { 32 this.id = id; 33 } 34 public int getAge() { 35 return age; 36 } 37 public void setAge(int age) { 38 this.age = age; 39 } 40 public String getName() { 41 return name; 42 } 43 public void setName(String name) { 44 this.name = name; 45 } 46 public Address getAddress() { 47 return address; 48 } 49 public void setAddress(Address address) { 50 this.address = address; 51 } 52 }
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.Embeddable; 7 import javax.persistence.JoinColumn; 8 import javax.persistence.OneToMany; 9 10 11 import org.hibernate.annotations.Parent; 12 13 @Embeddable 14 public class Address { 15 private Integer addressId; 16 private String addressDetail; 17 @Parent 18 private Person person; 19 @OneToMany(targetEntity=School.class) 20 @JoinColumn(name="address_id", referencedColumnName="person_id") 21 private Set<School> schools = new HashSet<>(); 22 public Integer getAddressId() { 23 return addressId; 24 } 25 public void setAddressId(Integer addressId) { 26 this.addressId = addressId; 27 } 28 public String getAddressDetail() { 29 return addressDetail; 30 } 31 public void setAddressDetail(String addressDetail) { 32 this.addressDetail = addressDetail; 33 } 34 public Address() {} 35 public Address(String addressDetail) { 36 this.addressDetail = addressDetail; 37 } 38 public Person getPerson() { 39 return person; 40 } 41 public void setPerson(Person person) { 42 this.person = person; 43 } 44 public Set<School> getSchools() { 45 return schools; 46 } 47 public void setSchools(Set<School> schools) { 48 this.schools = schools; 49 } 50 }
1 package map.six11; 2 3 import javax.persistence.Column; 4 import javax.persistence.Entity; 5 import javax.persistence.GeneratedValue; 6 import javax.persistence.GenerationType; 7 import javax.persistence.Id; 8 import javax.persistence.Table; 9 10 @Entity 11 @Table(name="school_inf") 12 public class School { 13 @Id @Column(name="person_id") 14 @GeneratedValue(strategy=GenerationType.IDENTITY) 15 private Integer schoolId; 16 private String schoolName; 17 18 public String getSchoolName() { 19 return schoolName; 20 } 21 22 public void setSchoolName(String schoolName) { 23 this.schoolName = schoolName; 24 } 25 26 public School(){} 27 28 public School(String schoolName) { 29 this.schoolName = schoolName; 30 } 31 32 public Integer getSchoolId() { 33 return schoolId; 34 } 35 36 public void setSchoolId(Integer schoolId) { 37 this.schoolId = schoolId; 38 } 39 }
基于复合主键的关联关系
虽然Hibernate不推荐复合主键,但依然提供了支持。下面的例子中,Person将会使用复合主键,在Person中有个Address集合属性,其元素也是持久化实体,同时还在Address端添加了Person的引用,因此Person与Address之间形成双向1-N关联关系。
Person类,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Entity; 8 import javax.persistence.Id; 9 import javax.persistence.OneToMany; 10 import javax.persistence.Table; 11 12 13 @Entity 14 @Table(name="person_inf") 15 public class Person implements java.io.Serializable{ 16 @Id 17 private String first; 18 @Id 19 private String last; 20 private int age; 21 @OneToMany(targetEntity=Address.class, mappedBy="person", cascade=CascadeType.ALL) 22 private Set<Address> addresses = new HashSet<>(); 23 24 public int getAge() { 25 return age; 26 } 27 public Set<Address> getAddresses() { 28 return addresses; 29 } 30 public void setAddresses(Set<Address> addresses) { 31 this.addresses = addresses; 32 } 33 public void setAge(int age) { 34 this.age = age; 35 } 36 public String getFirst() { 37 return first; 38 } 39 public void setFirst(String first) { 40 this.first = first; 41 } 42 public String getLast() { 43 return last; 44 } 45 public void setLast(String last) { 46 this.last = last; 47 } 48 public boolean equals(Object obj) { 49 if (this==obj) return true; 50 if (obj != null && obj.getClass() == Person.class) { 51 Person target = (Person)obj; 52 return target.getFirst().equals(this.first) && target.getLast().equals(this.last); 53 } 54 return false; 55 } 56 public int hashCode() { 57 return getFirst().hashCode() * 31 + getLast().hashCode(); 58 } 59 }
Address实体,
1 package map.six11; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.Column; 7 import javax.persistence.Embeddable; 8 import javax.persistence.Entity; 9 import javax.persistence.GeneratedValue; 10 import javax.persistence.GenerationType; 11 import javax.persistence.Id; 12 import javax.persistence.JoinColumn; 13 import javax.persistence.JoinColumns; 14 import javax.persistence.ManyToOne; 15 import javax.persistence.OneToMany; 16 17 18 19 20 import javax.persistence.Table; 21 22 import org.hibernate.annotations.Parent; 23 24 @Entity 25 @Table(name="address_inf") 26 public class Address implements java.io.Serializable { 27 @Id @Column(name="address_id") 28 @GeneratedValue(strategy=GenerationType.IDENTITY) 29 private Integer addressId; 30 private String addressDetail; 31 32 @ManyToOne(targetEntity=Person.class) 33 @JoinColumns({@JoinColumn(name="person_first", referencedColumnName="first", nullable=false), 34 @JoinColumn(name="person_last", referencedColumnName="last", nullable=false)}) 35 private Person person; 36 public Integer getAddressId() { 37 return addressId; 38 } 39 public void setAddressId(Integer addressId) { 40 this.addressId = addressId; 41 } 42 public String getAddressDetail() { 43 return addressDetail; 44 } 45 public void setAddressDetail(String addressDetail) { 46 this.addressDetail = addressDetail; 47 } 48 public Address() {} 49 public Address(String addressDetail) { 50 this.addressDetail = addressDetail; 51 } 52 public Person getPerson() { 53 return person; 54 } 55 public void setPerson(Person person) { 56 this.person = person; 57 } 58 }
测试类,
1 package map.six11; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 8 public class PersonManager { 9 public static void main(String[] args) { 10 Configuration conf = new Configuration().configure(); 11 //conf.addClass(Person.class); 12 //conf.addClass(School.class); 13 //conf.addClass(Address.class); 14 conf.addAnnotatedClass(Person.class); 15 //conf.addAnnotatedClass(School.class); 16 conf.addAnnotatedClass(Address.class); 17 SessionFactory sf = conf.buildSessionFactory(); 18 Session sess = sf.openSession(); 19 Transaction tx = sess.beginTransaction(); 20 21 Person p = new Person(); 22 p.setFirst("天王盖地虎"); 23 p.setLast("宝塔镇河妖"); 24 p.setAge(21); 25 26 Address a = new Address("广州天河"); 27 a.setPerson(p); 28 Address a2 = new Address("上海虹桥"); 29 a2.setPerson(p); 30 31 p.getAddresses().add(a); 32 p.getAddresses().add(a2); 33 sess.save(p); 34 35 tx.commit(); 36 sess.close(); 37 sf.close(); 38 } 39 }
因为在Person中设置了级联更新(cascade),因此在测试类中不需要显示地持久化Address。
Address中的引用属性person添加了nullable=false属性因此在需要在测试类中为preson属性赋值(a.setPerson(...))
执行测试类可以看到在address_inf表中,是通过两个外键(person_first, person_last)与person_inf表关联的,因为person_inf表是复合主键。
MariaDB [test]> select * from person_inf; +------------+------------+-----+ | last | first | age | +------------+------------+-----+ | 宝塔镇河妖 | 天王盖地虎 | 21 | +------------+------------+-----+ 1 row in set (0.00 sec) MariaDB [test]> select * from address_inf; +------------+---------------+-------------+--------------+ | address_id | addressDetail | person_last | person_first | +------------+---------------+-------------+--------------+ | 1 | 上海虹桥 | 宝塔镇河妖 | 天王盖地虎 | | 2 | 广州天河 | 宝塔镇河妖 | 天王盖地虎 | +------------+---------------+-------------+--------------+ 2 rows in set (0.00 sec)
表结构,
复合主键的成员属性为关联实体
这比前面的基于复合主键的关联关系更复杂了一层,但实际项目中会有很多这样的例子,例如下面这个经销存系统。
该系统涉及订单,商品,订单项三个实体,其中一个订单可以包含多个订单项,一个订单项用于订购某种商品,以及订购数量,一个商品可以包含在不同订单项中。
从上面介绍来看,订单和订单项存在双向1-N关系,订单项和商品之间存在单向N-1关系。
这里的订单项是一个关键实体,同时直接关联了订单和商品,在实际项目中很多程序员不为订单项额外地设计一个逻辑主键,而是用 订单主键+商品主键+订购数量 作为复合主键,Hibernate并不推荐这么做,因为这样增加了程序复杂性。 对于这样的设计,在Hibernate中就需要做一些特殊的映射了。
下面是订单类,
1 package map.six11; 2 3 import java.util.Date; 4 import java.util.HashSet; 5 import java.util.Set; 6 7 import javax.persistence.CascadeType; 8 import javax.persistence.Column; 9 import javax.persistence.Entity; 10 import javax.persistence.GeneratedValue; 11 import javax.persistence.GenerationType; 12 import javax.persistence.Id; 13 import javax.persistence.OneToMany; 14 import javax.persistence.Table; 15 16 @Entity 17 @Table(name="Order_inf") 18 public class Order { 19 @Id @Column(name="order_id") 20 @GeneratedValue(strategy=GenerationType.IDENTITY) 21 private Integer orderId; 22 private Date orderDate; 23 @OneToMany(targetEntity=OrderItem.class, mappedBy="order") 24 private Set<OrderItem> items = new HashSet<>(); 25 public Date getOrderDate() { 26 return orderDate; 27 } 28 public void setOrderDate(Date orderDate) { 29 this.orderDate = orderDate; 30 } 31 public Set<OrderItem> getItems() { 32 return items; 33 } 34 public void setItems(Set<OrderItem> items) { 35 this.items = items; 36 } 37 public Order() {} 38 public Order(Date orderDate) { 39 this.orderDate = orderDate; 40 } 41 }
订单类与订单项存在1-N双向关联,所以在订单类中有一个Set存放订单项,用@OneToMany与之关联并通过mappedBy设置订单类不控制关系。
与之对应的是在订单项类中,有一个订单类的引用,订单项与订单存在N-1关联,用@ManyToOne修饰,指定订单id为关联外键,
同时还需要有一个商品类的引用,订单项与商品也存在N-1关联,用 @ManyToOne修饰,指定商品id为关联外键,
在XML映射文件版本中,需要使用<key-many-to-one>来解决这种关联主键的成员又关联其他实体的情况,不过在JPA注解中则直接用 @ManyToOne + @Id即可,可见JPA确实比较简单,
1 package map.six11; 2 3 import javax.persistence.CascadeType; 4 import javax.persistence.Column; 5 import javax.persistence.Entity; 6 import javax.persistence.GeneratedValue; 7 import javax.persistence.GenerationType; 8 import javax.persistence.Id; 9 import javax.persistence.JoinColumn; 10 import javax.persistence.ManyToOne; 11 import javax.persistence.Table; 12 13 @Entity 14 @Table(name="order_item_inf") 15 public class OrderItem implements java.io.Serializable { 16 @ManyToOne(targetEntity=Order.class) 17 @JoinColumn(name="order_id", referencedColumnName="order_id") 18 @Id 19 private Order order; 20 @ManyToOne(targetEntity=Product.class) 21 @JoinColumn(name="product_id", referencedColumnName="product_id") 22 @Id 23 private Product product; 24 @Id 25 private int count; 26 public OrderItem() {} 27 public OrderItem(Order order, Product product, int count) { 28 this.order = order; 29 this.product = product; 30 this.count = count; 31 } 32 public boolean equals(Object obj) { 33 if (this == obj) return true; 34 if (obj != null && obj.getClass() == OrderItem.class) { 35 OrderItem target = (OrderItem)obj; 36 return this.order.equals(target.getOrder()) 37 && this.product.equals(target.getProduct()) 38 && this.count == target.getCount(); 39 } 40 return false; 41 } 42 public int hashCode() { 43 return (this.product == null ? 0 : this.product.hashCode()) * 31 * 31 + 44 (this.order == null ? 0 :this.order.hashCode()) * 31 45 + this.count; 46 } 47 public Order getOrder() { 48 return order; 49 } 50 public void setOrder(Order order) { 51 this.order = order; 52 } 53 public Product getProduct() { 54 return product; 55 } 56 public void setProduct(Product product) { 57 this.product = product; 58 } 59 public int getCount() { 60 return count; 61 } 62 public void setCount(int count) { 63 this.count = count; 64 } 65 66 }
商品类不需要主动与谁管理,因此只是一个普通实体,
1 package map.six11; 2 3 import javax.persistence.Column; 4 import javax.persistence.Entity; 5 import javax.persistence.GeneratedValue; 6 import javax.persistence.GenerationType; 7 import javax.persistence.Id; 8 import javax.persistence.Table; 9 10 @Entity 11 @Table(name="product_inf") 12 public class Product { 13 @Id @Column(name="product_id") 14 @GeneratedValue(strategy=GenerationType.IDENTITY) 15 private Integer productId; 16 private String name; 17 public Product() {} 18 public Product(String name) { 19 this.setName(name); 20 } 21 public Integer getProductId() { 22 return productId; 23 } 24 public void setProductId(Integer productId) { 25 this.productId = productId; 26 } 27 public String getName() { 28 return name; 29 } 30 public void setName(String name) { 31 this.name = name; 32 } 33 }
下面是一个测试类,
1 package map.six11; 2 3 import java.util.Date; 4 5 import org.hibernate.Session; 6 import org.hibernate.SessionFactory; 7 import org.hibernate.Transaction; 8 import org.hibernate.cfg.Configuration; 9 10 public class PersonManager { 11 public static void main(String[] args) { 12 Configuration conf = new Configuration().configure(); 13 //conf.addClass(Person.class); 14 //conf.addClass(School.class); 15 //conf.addClass(Address.class); 16 //conf.addAnnotatedClass(Person.class); 17 //conf.addAnnotatedClass(School.class); 18 //conf.addAnnotatedClass(Address.class); 19 conf.addAnnotatedClass(Product.class); 20 conf.addAnnotatedClass(Order.class); 21 conf.addAnnotatedClass(OrderItem.class); 22 SessionFactory sf = conf.buildSessionFactory(); 23 Session sess = sf.openSession(); 24 Transaction tx = sess.beginTransaction(); 25 26 Product p1 = new Product("手机"); 27 Product p2 = new Product("电脑"); 28 sess.save(p1); 29 sess.save(p2); 30 31 Order o = new Order(new Date()); 32 33 OrderItem i1 = new OrderItem(o, p1, 3); 34 OrderItem i2 = new OrderItem(o, p2, 5); 35 36 o.getItems().add(i1); 37 o.getItems().add(i2); 38 39 sess.save(o); 40 41 sess.save(i1); 42 sess.save(i2); 43 44 tx.commit(); 45 sess.close(); 46 sf.close(); 47 } 48 }
执行测试类,生成了3个表,
Hibernate生成的SQL如下,
1 Hibernate: insert into product_inf (name) values (?) 2 Hibernate: insert into product_inf (name) values (?) 3 Hibernate: insert into Order_inf (orderDate) values (?) 4 Hibernate: insert into order_item_inf (product_id, order_id, count) values (?, ?, ?) 5 Hibernate: insert into order_item_inf (product_id, order_id, count) values (?, ?, ?)
表数据,
MariaDB [test]> select * from product_inf; +------------+------+ | product_id | name | +------------+------+ | 1 | 手机 | | 2 | 电脑 | +------------+------+ 2 rows in set (0.00 sec) MariaDB [test]> select * from order_item_inf; +-------+------------+----------+ | count | product_id | order_id | +-------+------------+----------+ | 3 | 1 | 1 | | 5 | 2 | 1 | +-------+------------+----------+ 2 rows in set (0.00 sec) MariaDB [test]> select * from order_inf; +----------+---------------------+ | order_id | orderDate | +----------+---------------------+ | 1 | 2017-01-10 16:45:31 | +----------+---------------------+ 1 row in set (0.00 sec)
表结构,
MariaDB [test]> desc order_item_inf; +------------+---------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +------------+---------+------+-----+---------+-------+ | count | int(11) | NO | PRI | NULL | | | product_id | int(11) | NO | PRI | NULL | | | order_id | int(11) | NO | PRI | NULL | | +------------+---------+------+-----+---------+-------+ 3 rows in set (0.05 sec)
表关联如下,
级联操作(持久化的传播性)
级联操作即一个实体状态改变时(update,delete。。。),与之关联的实体也会自动改变状态,这可以简化编程。
在Hibernate中,对于两个表的关联有两种实现方式,一种是把关联的表(从表)作为主表的组件,另一种是把从表映射为新的持久化类,
对于将从表记录映射为主表(持久化类)的组件这种情况,即上一章说的”集合属性的元素是组件“的情形,这些组件的生命周期总是依赖于父对象,Hibernate默认就会启用级联操作。
而对于将关联的表映射问单独的持久化类的情况,Hibernate默认不启用级联操作,需要手动开启,可以在@OneToMany, @OneToOne, @ManyToMany中使用cascade属性来指定。
cascade有以下五种可选值,
CascadeTpye.ALL
CascadeType.MERAGE
CascadeType.PERSIST
CascadeType.REFRESH
CascadeType.REMOVE
通常不会在@ManyToOne中使用级联属性,因为这没有任何意义,从表的改变不应该影响到主表。
除了cascade,还有一种特殊的级联策略,即orphanRemoval属性,如果在@OneToMany等注解中设置了这个属性(true或false),则会删除”孤儿“记录。(所谓孤儿记录,就是当程序通过主表实体切断与从表的关联关系,则从表实体就成了孤儿)、