Hibernate —— 映射关联关系
一、映射多对一关联关系。
1.单向的多对一
(1)以 Customer 和 Order 为例:一个用户可以发出多个订单,而一个订单只能属于一个客户。从 Order 到 Customer 是多对一关联关系。
(2)创建 Customer 和 Order 表。
CREATE TABLE customer ( customer_id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY , customer_name VARCHAR(50) ) CREATE TABLE `order` ( order_id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, order_name VARCHAR(50), customer_id INT(11) )
(3)用 Intellij Idea 自动生成关联关系,以及对应的 Entitiy.hbm.xml 和 持久化类。
说明:
其中 Type 是用来修饰对应的 Attribute Name 的。
在 Order 端,定义 Customer 类,一个订单属于一个客户。而在 Customer 端,一个客户可以有多个订单,因为是单向的,所以这里放弃属性的添加。
在 Join Columns 定义了 Order 和 Customer 之间的关联关系,order 表中的 customer_id 外键和 customer 表中的 customer_id 主键关联。
来看生成的 Schema:
没有勾选 customer_id,是因为 Intellij Idea 没法直接映射为 Customer 类型的 customer。
<hibernate-mapping> <class name="com.nucsoft.hibernate.Order" table="order" schema="hibernate"> <id name="orderId"> <column name="order_id" sql-type="int(11)"/> <generator class="native"/> </id> <property name="orderName"> <column name="order_name" sql-type="varchar(50)" length="50" not-null="true"/> </property> <many-to-one name="customer" class="com.nucsoft.hibernate.Customer"> <column name="customer_id" not-null="true"/> </many-to-one> </class> </hibernate-mapping>
使用 <many-to-one> 节点来维护多对一关联关系。
name 属性:多这一端关联的一那一端的属性的名称。
class 属性:关联的一端的属性的类型。
column 属性:一那一端在多的一端对应的数据表中的外键。可以任意命名,但需要和数据表中的字段对应。
(4)单向多对一的 CRUD 以及需要注意的问题。
<1> 新增
①先保存一的一端 Customer,后保存多的一端 Order。
@Test public void testMany2OneSave() { Customer customer = new Customer(); customer.setCustomerName("aa"); Order order = new Order(); order.setOrderName("order1"); order.setCustomer(customer); Order order2 = new Order(); order2.setOrderName("order2"); order2.setCustomer(customer); session.save(customer); session.save(order); session.save(order2); }
打印 SQL:
Hibernate: insert into hibernate.customer (customer_name) values (?) Hibernate: insert into hibernate.order (order_name, customer_id) values (?, ?) Hibernate: insert into hibernate.order (order_name, customer_id) values (?, ?)
结论:发送了3条 INSERT 语句。
②先保存多的一端 Order,再保存一的一端 Customer。
@Test public void testMany2OneSave() { Customer customer = new Customer(); customer.setCustomerName("bb"); Order order = new Order(); order.setOrderName("order3"); order.setCustomer(customer); Order order2 = new Order(); order2.setOrderName("order4"); order2.setCustomer(customer); session.save(order); session.save(order2); session.save(customer); }
打印 SQL:
Hibernate: insert into hibernate.order (order_name, customer_id) values (?, ?) Hibernate: insert into hibernate.order (order_name, customer_id) values (?, ?) Hibernate: insert into hibernate.customer (customer_name) values (?) Hibernate: update hibernate.order set order_name=?, customer_id=? where order_id=? Hibernate: update hibernate.order set order_name=?, customer_id=? where order_id=?
结论:发送了3条 INSERT 语句,2条 UPDATE 语句。
总结:在单向多对一的关联关系下,先插入 1 的一端会减少 SQL 语句的执行,性能更高。
<2>删除
先删除1的一端。
@Test public void testMany2OneDelete() { Customer customer = (Customer) session.get(Customer.class, 1); session.delete(customer); }
控制台打印:
Cannot delete or update a parent row: a foreign key constraint fails (`hibernate`.`order`, CONSTRAINT `FK_m6q2ofkj1g5aobtb2p00ajpqg` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`customer_id`))
结论:在不设置级联关系的前提下,不能删除 1 的一端。
<3>更新
@Test public void testMany2OneUpdate() { Order order = (Order) session.get(Order.class, 1); order.getCustomer().setCustomerName("aaa"); }
Hibernate: select order0_.order_id as order_id1_1_0_, order0_.order_name as order_na2_1_0_, order0_.customer_id as customer3_1_0_ from hibernate.order order0_ where order0_.order_id=? Hibernate: select customer0_.customer_id as customer1_0_0_, customer0_.customer_name as customer2_0_0_ from hibernate.customer customer0_ where customer0_.customer_id=? Hibernate: update hibernate.customer set customer_name=? where customer_id=?
<4>查询
①查询 n 的一端,但是不使用查询出来关联的 1 的一端的对象。
@Test public void testMany2OneGet() { Order order = (Order) session.get(Order.class, 1); System.out.println(order.getCustomer().getClass().getName()); }
Hibernate: select order0_.order_id as order_id1_1_0_, order0_.order_name as order_na2_1_0_, order0_.customer_id as customer3_1_0_ from hibernate.order order0_ where order0_.order_id=? order1 com.nucsoft.hibernate.Customer_$$_jvst30c_1
②查询 n 的一端,使用查询出来关联的 1 的一端的对象。
@Test public void testMany2OneGet() { Order order = (Order) session.get(Order.class, 1); System.out.println(order.getCustomer().getClass().getName()); order.getCustomer().getCustomerName(); }
Hibernate: select order0_.order_id as order_id1_1_0_, order0_.order_name as order_na2_1_0_, order0_.customer_id as customer3_1_0_ from hibernate.order order0_ where order0_.order_id=? com.nucsoft.hibernate.Customer_$$_jvst30c_1 Hibernate: select customer0_.customer_id as customer1_0_0_, customer0_.customer_name as customer2_0_0_ from hibernate.customer customer0_ where customer0_.customer_id=?
总结:可以发现,采用的是懒加载机制,即获取到的 1 的一端的对象是一个代理对象。只有在使用这个对象的属性的情况下,才会发送 SQL 语句。
③ 由懒加载机制引发的 懒加载异常。
@Test public void testMany2OneGet() { Order order = (Order) session.get(Order.class, 1); System.out.println(order.getCustomer().getClass().getName()); session.close(); order.getCustomer().getCustomerName(); }
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
在需要使用对象之前,关闭了 Session 连接,由此会引发 LazyInitializationException 异常。
2.双向的多对一
(1)还是以 Order 和 Customer 为例:双向的多对一不仅仅要在 Order 类中定义一个 Customer 属性,而在 Customer 类中也需定义存放 Order 对象的集合属性。
(2)创建 Order 和 Customer 表和创建单向多对一相同。
(3)通过 Intellij Idea 生成简单的持久化类和 Entity.hbm.xml 文件。手动的去建立关联关系。
<1>生成简单的持久化类 和 Entity.hbm.xml 文件
package com.nucsoft.hibernate; /** * @author solverpeng * @create 2016-10-11-13:23 */ public class Customer { private Integer customerId; private String customerName; public Integer getCustomerId() { return customerId; } public void setCustomerId(Integer customerId) { this.customerId = customerId; } public String getCustomerName() { return customerName; } public void setCustomerName(String customerName) { this.customerName = customerName; } @Override public boolean equals(Object o) { if(this == o) { return true; } if(o == null || getClass() != o.getClass()) { return false; } Customer customer = (Customer) o; if(customerId != null ? !customerId.equals(customer.customerId) : customer.customerId != null) { return false; } if(customerName != null ? !customerName.equals(customer.customerName) : customer.customerName != null) { return false; } return true; } @Override public int hashCode() { int result = customerId != null ? customerId.hashCode() : 0; result = 31 * result + (customerName != null ? customerName.hashCode() : 0); return result; } }
package com.nucsoft.hibernate; /** * @author solverpeng * @create 2016-10-11-13:23 */ public class Order { private Integer orderId; private String orderName; public Integer getOrderId() { return orderId; } public void setOrderId(Integer orderId) { this.orderId = orderId; } public String getOrderName() { return orderName; } public void setOrderName(String orderName) { this.orderName = orderName; } @Override public boolean equals(Object o) { if(this == o) { return true; } if(o == null || getClass() != o.getClass()) { return false; } Order order = (Order) o; if(orderId != null ? !orderId.equals(order.orderId) : order.orderId != null) { return false; } if(orderName != null ? !orderName.equals(order.orderName) : order.orderName != null) { return false; } return true; } @Override public int hashCode() { int result = orderId != null ? orderId.hashCode() : 0; result = 31 * result + (orderName != null ? orderName.hashCode() : 0); return result; } }
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.nucsoft.hibernate.Customer" table="customer" schema="hibernate"> <id name="customerId"> <column name="customer_id" sql-type="int(11)"/> </id> <property name="customerName"> <column name="customer_name" sql-type="varchar(50)" length="50" not-null="true"/> </property> </class> </hibernate-mapping>
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.nucsoft.hibernate.Order" table="order" schema="hibernate"> <id name="orderId"> <column name="order_id" sql-type="int(11)"/> </id> <property name="orderName"> <column name="order_name" sql-type="varchar(50)" length="50" not-null="true"/> </property> </class> </hibernate-mapping>
<2>手动建立关联关系
①在 Order 一端建立多对一的关联关系。
- 在 Order 持久化类中添加 Customer 类型的一个属性 customer。
- 在 Order.hbm.xml 文件中添加多对一的关联关系。同时修改主键生成方式为 native。
②在 Customer 一端建立一对多的关联关系。
- 在 Customer 持久化类中添加 Order 的一个集合 orders。
- 在 Customer.hbm.xml 添加一对多的关联关系。同时修改主键生成方式为 native。
③详细说明:在 Customer.hbm.xml 文件中添加一对多的关联关系。
- 当 Session 从数据库中加载 Java 集合时,创建的是 Hibernate 内置的集合类的实例。因此在持久化类中定义集合属性时需要定义成接口类型,不能是具体的某个实现类。
- Hibernate 内置的集合具有集合代理功能,因为有代理功能,所以支持延迟检索策略。
- 在定义集合的时候,通常将其初始化为集合实现类的一个实例,防止 NullPointerException。
- Hibernate 使用 <set> 元素来映射 Set 类型的属性。
- 1 的一端的 Set 类型属性数据还是存放在 n 的一端。
④ set 元素
- name 属性:待映射的 Set 类型的属性的属性名称。
- table 属性:待映射的 Set 属性的泛型类型所对应的表。
- key 子元素:column 属性,多的一端的外键名称。
- one-to-many 子元素:class 属性,n 的一端的持久化类名称。
对应关系如图。
⑤最终的实体类和 Entity.hbm.xml 文件。
package com.nucsoft.hibernate; import java.util.HashSet; import java.util.Set; /** * @author solverpeng * @create 2016-10-11-13:23 */ public class Customer { private Integer customerId; private String customerName; private Set<Order> orders = new HashSet<>(); public Set<Order> getOrders() { return orders; } public void setOrders(Set<Order> orders) { this.orders = orders; } public Integer getCustomerId() { return customerId; } public void setCustomerId(Integer customerId) { this.customerId = customerId; } public String getCustomerName() { return customerName; } public void setCustomerName(String customerName) { this.customerName = customerName; } @Override public boolean equals(Object o) { if(this == o) { return true; } if(o == null || getClass() != o.getClass()) { return false; } Customer customer = (Customer) o; if(customerId != null ? !customerId.equals(customer.customerId) : customer.customerId != null) { return false; } if(customerName != null ? !customerName.equals(customer.customerName) : customer.customerName != null) { return false; } return true; } @Override public int hashCode() { int result = customerId != null ? customerId.hashCode() : 0; result = 31 * result + (customerName != null ? customerName.hashCode() : 0); return result; } }
package com.nucsoft.hibernate; /** * @author solverpeng * @create 2016-10-11-13:23 */ public class Order { private Integer orderId; private String orderName; private Customer customer; public Customer getCustomer() { return customer; } public void setCustomer(Customer customer) { this.customer = customer; } public Integer getOrderId() { return orderId; } public void setOrderId(Integer orderId) { this.orderId = orderId; } public String getOrderName() { return orderName; } public void setOrderName(String orderName) { this.orderName = orderName; } @Override public boolean equals(Object o) { if(this == o) { return true; } if(o == null || getClass() != o.getClass()) { return false; } Order order = (Order) o; if(orderId != null ? !orderId.equals(order.orderId) : order.orderId != null) { return false; } if(orderName != null ? !orderName.equals(order.orderName) : order.orderName != null) { return false; } return true; } @Override public int hashCode() { int result = orderId != null ? orderId.hashCode() : 0; result = 31 * result + (orderName != null ? orderName.hashCode() : 0); return result; } }
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="com.nucsoft.hibernate"> <class name="Customer" table="customer" schema="hibernate"> <id name="customerId"> <column name="customer_id" sql-type="int(11)"/> <generator class="native"/> </id> <property name="customerName"> <column name="customer_name" sql-type="varchar(50)" length="50" not-null="true"/> </property> <set name="orders" table="order"> <key column="customer_id"/> <one-to-many class="Order"/> </set> </class> </hibernate-mapping>
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="com.nucsoft.hibernate"> <class name="Order" table="order" schema="hibernate"> <id name="orderId"> <column name="order_id" sql-type="int(11)"/> <generator class="native"/> </id> <property name="orderName"> <column name="order_name" sql-type="varchar(50)" length="50" not-null="true"/> </property> <many-to-one name="customer" class="Customer" column="customer_id"/> </class> </hibernate-mapping>
(4)通过 Intellij Idea 直接生成双向的多对一的关联关系。
<1>为生成的每个 Entity.hbm.xml 文件添加主键生成方式。
<2>为 Customer 类中的 orders 属性进行初始化。
<3>最终的持久化类和 Entity.hbm.xml。
package com.nucsoft.hibernate; import java.util.HashSet; import java.util.Set; /** * @author solverpeng * @create 2016-10-11-14:01 */ public class Customer { private Integer customerId; private String customerName; private Set<Order> orders = new HashSet<>(); public Integer getCustomerId() { return customerId; } public void setCustomerId(Integer customerId) { this.customerId = customerId; } public String getCustomerName() { return customerName; } public void setCustomerName(String customerName) { this.customerName = customerName; } @Override public boolean equals(Object o) { if(this == o) { return true; } if(o == null || getClass() != o.getClass()) { return false; } Customer customer = (Customer) o; if(customerId != null ? !customerId.equals(customer.customerId) : customer.customerId != null) { return false; } if(customerName != null ? !customerName.equals(customer.customerName) : customer.customerName != null) { return false; } return true; } @Override public int hashCode() { int result = customerId != null ? customerId.hashCode() : 0; result = 31 * result + (customerName != null ? customerName.hashCode() : 0); return result; } public Set<Order> getOrders() { return orders; } public void setOrders(Set<Order> orders) { this.orders = orders; } }
package com.nucsoft.hibernate; /** * @author solverpeng * @create 2016-10-11-14:01 */ public class Order { private Integer orderId; private String orderName; private Customer customer; public Integer getOrderId() { return orderId; } public void setOrderId(Integer orderId) { this.orderId = orderId; } public String getOrderName() { return orderName; } public void setOrderName(String orderName) { this.orderName = orderName; } @Override public boolean equals(Object o) { if(this == o) { return true; } if(o == null || getClass() != o.getClass()) { return false; } Order order = (Order) o; if(orderId != null ? !orderId.equals(order.orderId) : order.orderId != null) { return false; } if(orderName != null ? !orderName.equals(order.orderName) : order.orderName != null) { return false; } return true; } @Override public int hashCode() { int result = orderId != null ? orderId.hashCode() : 0; result = 31 * result + (orderName != null ? orderName.hashCode() : 0); return result; } public Customer getCustomer() { return customer; } public void setCustomer(Customer customer) { this.customer = customer; } }
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.nucsoft.hibernate.Customer" table="customer" schema="hibernate"> <id name="customerId"> <column name="customer_id" sql-type="int(11)"/> <generator class="native"/> </id> <property name="customerName"> <column name="customer_name" sql-type="varchar(50)" length="50" not-null="true"/> </property> <set name="orders" inverse="true"> <key> <column name="customer_id" not-null="true"/> </key> <one-to-many not-found="ignore" class="com.nucsoft.hibernate.Order"/> </set> </class> </hibernate-mapping>
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.nucsoft.hibernate.Order" table="order" schema="hibernate"> <id name="orderId"> <column name="order_id" sql-type="int(11)"/> <generator class="native"/> </id> <property name="orderName"> <column name="order_name" sql-type="varchar(50)" length="50" not-null="true"/> </property> <many-to-one name="customer" class="com.nucsoft.hibernate.Customer"> <column name="customer_id" not-null="true"/> </many-to-one> </class> </hibernate-mapping>
<4>对比发现,通过 Intellij Idea 自动生成的 Customer.hbm.xml 文件中 set 元素多了一个 inverse 属性。稍后进行说明。
(5)双向多对一的 CRUD 和需要注意的问题
<1>新增
①双方都维护关联关系,即没有设置 inverse 属性,且没有添加非空约束。
先保存 1 的一端,再保存 n 的一端。
@Test public void testMany2OneBothSave() { Customer customer = new Customer(); customer.setCustomerName("aa"); Order order = new Order(); order.setOrderName("order1"); order.setCustomer(customer); Order order2 = new Order(); order2.setOrderName("order2"); order2.setCustomer(customer); customer.getOrders().add(order); customer.getOrders().add(order2); session.save(customer); session.save(order); session.save(order2); }
打印SQL:
Hibernate: insert into hibernate.customer (customer_name) values (?) Hibernate: insert into hibernate.order (order_name, customer_id) values (?, ?) Hibernate: insert into hibernate.order (order_name, customer_id) values (?, ?) Hibernate: update hibernate.order set customer_id=? where order_id=? Hibernate: update hibernate.order set customer_id=? where order_id=?
结果:打印了 3 条 INSERT 语句,2 条 UPDATE 语句
先保存 n 的一端,再保存 1 的一端。
@Test public void testMany2OneBothSave() { Customer customer = new Customer(); customer.setCustomerName("cc"); Order order = new Order(); order.setOrderName("order5"); order.setCustomer(customer); Order order2 = new Order(); order2.setOrderName("order6"); order2.setCustomer(customer); customer.getOrders().add(order); customer.getOrders().add(order2); session.save(order); session.save(order2); session.save(customer); }
打印 SQL :
Hibernate: insert into hibernate.order (order_name, customer_id) values (?, ?) Hibernate: insert into hibernate.order (order_name, customer_id) values (?, ?) Hibernate: insert into hibernate.customer (customer_name) values (?) Hibernate: update hibernate.order set order_name=?, customer_id=? where order_id=? Hibernate: update hibernate.order set order_name=?, customer_id=? where order_id=? Hibernate: update hibernate.order set customer_id=? where order_id=? Hibernate: update hibernate.order set customer_id=? where order_id=?
结果:打印了 3 条 INSERT 语句,4 条 UPDATE 语句。原因,双方都维护这关联关系。
②双方都维护关联关系,即没有设置 inverse 属性,对 order 表中的 customer_id 列添加非空约束(需要更改两个地方)。
先保存 n 的一端,再保存 1 的一端,会抛出异常。
org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation : com.nucsoft.hibernate.Order.customer -> com.nucsoft.hibernate.Customer
③ 1 的一端放弃维护关联关系,只由 n 的一端来维护。即设置 Customer.hbm.xml 的 set 元素 inverse 属性值为 true。
先保存 1 的一端,后保存 n 的一端。
Hibernate: insert into hibernate.customer (customer_name) values (?) Hibernate: insert into hibernate.order (order_name, customer_id) values (?, ?) Hibernate: insert into hibernate.order (order_name, customer_id) values (?, ?)
结果:只会发送3条 INSERT 语句。
④总结:
介绍了双向的多对一的下的保存操作,若都维护关联关系,则会多出 UPDATE 语句。且若外键存在非空约束时,不能先保存 n 的一端。
所以在进行 Hibernate 双向多对一保存的时候,最好的做法就是:
1 的一端放弃维护关联关系,即 设置 set 节点的 inverse 属性为 true。同时在保存的时候先保存 1 的一端,后保存 n 的一端。
<2>删除
@Test public void testMany2OneBothDelete() { Customer customer = (Customer) session.get(Customer.class, 5); session.delete(customer); }
同删除单向的多对一相同,会抛出异常:
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails
存在外键约束。
<3>更新
@Test public void testMany2OneBothUpdate() { Customer customer = (Customer) session.get(Customer.class, 5); System.out.println(customer.getOrders().iterator().next().getOrderName()); customer.getOrders().iterator().next().setOrderName("order@@"); }
Hibernate: select customer0_.customer_id as customer1_0_0_, customer0_.customer_name as customer2_0_0_ from hibernate.customer customer0_ where customer0_.customer_id=? Hibernate: select orders0_.customer_id as customer3_0_0_, orders0_.order_id as order_id1_1_0_, orders0_.order_id as order_id1_1_1_, orders0_.order_name as order_na2_1_1_, orders0_.customer_id as customer3_1_1_ from hibernate.order orders0_ where orders0_.customer_id=? order4 Hibernate: update hibernate.order set order_name=?, customer_id=? where order_id=?
<4>查询
@Test public void testMany2OneBothGet() { Customer customer = (Customer) session.get(Customer.class, 5); System.out.println(customer.getOrders().getClass()); }
打印结果:
Hibernate: select customer0_.customer_id as customer1_0_0_, customer0_.customer_name as customer2_0_0_ from hibernate.customer customer0_ where customer0_.customer_id=? class org.hibernate.collection.internal.PersistentSet
并没有查询关联的 Order 集合,实际类型为 Hibernate 内置的一个 Set 实现类。
证明了:
当 Session 从数据库中加载 Java 集合时,创建的是 Hibernate 内置的集合类的实例。因此在持久化类中定义集合属性时需要定义成接口类型,不能是具体的某个实现类。
也证明了:
Hibernate 内置的集合具有集合代理功能,因为有代理功能,所以支持延迟检索策略。
<5>说明
这里没有介绍 cascade 属性,是因为在实际的项目中,为了保护数据,很少设置 cascade 属性,而是手动去处理。
二、映射一对一关联关系。
1.这里只介绍双向的一对一关联关系如何映射。单向的一对一只需要把没有外键的一端去掉就好了。
2.基于外键映射的双向一对一
(1)以 Dempartment 和 Manager 为例。一个部门只能有一个经理,一个经理职能管理一个部门。
(2)创建 Department 和 Manager 表。在 Department 表建立 Manager 表的外键。
CREATE TABLE department ( dept_id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, dept_name VARCHAR(50), manager_id_fk INT(11) );
CREATE TABLE manager ( manager_id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT, manager_name VARCHAR(50) );
(3)通过 Intellij Idea 自动生成的双向 1 对 1 无法映射列。这里通过 Intellij Idea 生成简单的持久化类和 Entity.hbm.xml 文件,然后手动的添加映射。
package com.nucsoft.hibernate; /** * @author solverpeng * @create 2016-10-12-9:59 */ public class Department { private Integer deptId; private String deptName; private Manager manager; public Integer getDeptId() { return deptId; } public void setDeptId(Integer deptId) { this.deptId = deptId; } public String getDeptName() { return deptName; } public void setDeptName(String deptName) { this.deptName = deptName; } @Override public boolean equals(Object o) { if(this == o) { return true; } if(o == null || getClass() != o.getClass()) { return false; } Department that = (Department) o; if(deptId != null ? !deptId.equals(that.deptId) : that.deptId != null) { return false; } if(deptName != null ? !deptName.equals(that.deptName) : that.deptName != null) { return false; } return true; } @Override public int hashCode() { int result = deptId != null ? deptId.hashCode() : 0; result = 31 * result + (deptName != null ? deptName.hashCode() : 0); return result; } public Manager getManager() { return manager; } public void setManager(Manager manager) { this.manager = manager; } }
package com.nucsoft.hibernate; /** * @author solverpeng * @create 2016-10-12-9:59 */ public class Manager { private Integer managerId; private String managerName; private Department dept; public Integer getManagerId() { return managerId; } public void setManagerId(Integer managerId) { this.managerId = managerId; } public String getManagerName() { return managerName; } public void setManagerName(String managerName) { this.managerName = managerName; } @Override public boolean equals(Object o) { if(this == o) { return true; } if(o == null || getClass() != o.getClass()) { return false; } Manager manager = (Manager) o; if(managerId != null ? !managerId.equals(manager.managerId) : manager.managerId != null) { return false; } if(managerName != null ? !managerName.equals(manager.managerName) : manager.managerName != null) { return false; } return true; } @Override public int hashCode() { int result = managerId != null ? managerId.hashCode() : 0; result = 31 * result + (managerName != null ? managerName.hashCode() : 0); return result; } public Department getDept() { return dept; } public void setDept(Department dept) { this.dept = dept; } }
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.nucsoft.hibernate.Department" table="department" schema="hibernate"> <id name="deptId" column="dept_id"> <generator class="native"/> </id> <property name="deptName" column="dept_name"/> <many-to-one name="manager" class="com.nucsoft.hibernate.Manager" column="manager_id_fk" unique="true"/> </class> </hibernate-mapping>
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="com.nucsoft.hibernate.Manager" table="manager" schema="hibernate"> <id name="managerId" column="manager_id"> <generator class="native"/> </id> <property name="managerName" column="manager_name"/> <one-to-one name="dept" class="com.nucsoft.hibernate.Department" property-ref="manager"/> </class> </hibernate-mapping>
(4)说明:
在 department 表中来维护外键。在映射的时候选择 <many-to-one > 元素,通过 unique 属性来达到 一对一的效果。
在 manager 表中没有维护外键。在映射时候选择 <one-to-one>元素,至于属性 property-ref 稍后在查询的时候说明。
(5)CRUD 以及需要注意的地方。
<1>save
@Test public void testSave() { Department department = new Department(); department.setDeptName("dept-4"); Manager manager = new Manager(); manager.setManagerName("manager=DD"); department.setManager(manager); manager.setDept(department); session.save(manager); session.save(department); }
Hibernate: insert into hibernate.manager (manager_name) values (?) Hibernate: insert into hibernate.department (dept_name, manager_id_fk) values (?, ?)
@Test public void testSave() { Department department = new Department(); department.setDeptName("dept-2"); Manager manager = new Manager(); manager.setManagerName("manager=BB"); department.setManager(manager); manager.setDept(department); session.save(department); session.save(manager); }
Hibernate: insert into hibernate.department (dept_name, manager_id_fk) values (?, ?) Hibernate: insert into hibernate.manager (manager_name) values (?) Hibernate: update hibernate.department set dept_name=?, manager_id_fk=? where dept_id=?
对比发现,先保存没有外键列的对象,会减少 UPDATE 语句的发送,提高性能。
<2>delete
@Test public void testDelete() { Department department = (Department) session.get(Department.class, 4); session.delete(department); }
Hibernate: select department0_.dept_id as dept1_0_0_, department0_.dept_name as dept2_0_0_, department0_.manager_id_fk as manager3_0_0_ from hibernate.department department0_ where department0_.dept_id=? Hibernate: delete from hibernate.department where dept_id=?
@Test public void testDelete() { Manager manager = (Manager) session.get(Manager.class, 5); session.delete(manager); }
Cannot delete or update a parent row: a foreign key constraint fails (`hibernate`.`department`, CONSTRAINT `FK_kpcmf8csabfn9epikikcfqbk0` FOREIGN KEY (`manager_id_fk`) REFERENCES `manager` (`manager_id`))
外键关联对象存在的情况下,不能先删除拥有外键的对象。
<3>update
@Test public void testUpdate() { Department department = (Department) session.get(Department.class, 6); department.getManager().setManagerName("@@"); }
Hibernate: select department0_.dept_id as dept1_0_0_, department0_.dept_name as dept2_0_0_, department0_.manager_id_fk as manager3_0_0_ from hibernate.department department0_ where department0_.dept_id=? Hibernate: select manager0_.manager_id as manager1_1_1_, manager0_.manager_name as manager2_1_1_, department1_.dept_id as dept1_0_0_, department1_.dept_name as dept2_0_0_, department1_.manager_id_fk as manager3_0_0_ from hibernate.manager manager0_ left outer join hibernate.department department1_ on manager0_.manager_id=department1_.dept_id where manager0_.manager_id=? Hibernate: update hibernate.manager set manager_name=? where manager_id=?
<4>get
@Test public void testGet() { Department department = (Department) session.get(Department.class, 6); System.out.println(department); System.out.println(department.getManager().getClass()); System.out.println("---------------"); System.out.println(department.getManager().getManagerName()); }
Hibernate: select department0_.dept_id as dept1_0_0_, department0_.dept_name as dept2_0_0_, department0_.manager_id_fk as manager3_0_0_ from hibernate.department department0_ where department0_.dept_id=? com.nucsoft.hibernate.Department@b0688765 class com.nucsoft.hibernate.Manager_$$_javassist_1 --------------- Hibernate: select manager0_.manager_id as manager1_1_1_, manager0_.manager_name as manager2_1_1_, department1_.dept_id as dept1_0_0_, department1_.dept_name as dept2_0_0_, department1_.manager_id_fk as manager3_0_0_ from hibernate.manager manager0_ left outer join hibernate.department department1_ on manager0_.manager_id=department1_.dept_id where manager0_.manager_id=? @@
可以看到, 查询拥有外键对象关联的对象时,采用的还是懒加载机制。此种情况下,若 session 关闭,再去调用关联对象的某个属性,会发生懒加载异常。
查询双向一对一中没有外键的一端:
<one-to-one name="dept" class="com.nucsoft.hibernate.Department"/>
@Test public void testGet2() { Manager manager = (Manager) session.get(Manager.class, 6); System.out.println(manager); }
Hibernate: select manager0_.manager_id as manager1_1_1_, manager0_.manager_name as manager2_1_1_, department1_.dept_id as dept1_0_0_, department1_.dept_name as dept2_0_0_, department1_.manager_id_fk as manager3_0_0_ from hibernate.manager manager0_ left outer join hibernate.department department1_ on manager0_.manager_id=department1_.dept_id where manager0_.manager_id=? com.nucsoft.hibernate.Manager@8ba
<one-to-one name="dept" class="com.nucsoft.hibernate.Department" property-ref="manager"/>
Hibernate: select manager0_.manager_id as manager1_1_1_, manager0_.manager_name as manager2_1_1_, department1_.dept_id as dept1_0_0_, department1_.dept_name as dept2_0_0_, department1_.manager_id_fk as manager3_0_0_ from hibernate.manager manager0_ left outer join hibernate.department department1_ on manager0_.manager_id=department1_.manager_id_fk where manager0_.manager_id=? com.nucsoft.hibernate.Manager@8ba
可以发现,在第一次查询时,没有设置 property-ref 属性。左外链接查询时,虽然结果正确,但是连接条件不正确。
至于说,为什么查询 Manager 对象的时候,使用了左外链接而不是懒加载,因为 Manager 端没有 Deparment 的外键。它不知道谁与它有关系。只能通过左外链接查询一次查询。
3.基于主键的双向的一对一
(1)本质上和基于外键的双向一对一关联一样,只不过是以主键作为了外键来使用的。
(2)还是以 Department 和 Manager 为例。
(3)deparment 表做了改动,因为是基于主键的映射,这里将 department 表中 manager_id_fk 去掉了。
(4)Department.hbm.xml 和 Manager.hbm.xml 文件
<hibernate-mapping> <class name="com.nucsoft.hibernate.Department" table="department" schema="hibernate"> <id name="deptId" column="dept_id"> <generator class="foreign"> <param name="property">manager</param> </generator> </id> <property name="deptName" column="dept_name"/> <one-to-one name="manager" class="com.nucsoft.hibernate.Manager" constrained="true"/> </class> </hibernate-mapping>
对于 Department.hbm.xml 文件,主键生成方式改为 foreign,并且指定了主键生成所依赖的属性所对应的持久化类的主键生成方式。
需要注意的是,需要在 <one-to-one>节点添加 constrained 属性为true。
<hibernate-mapping> <class name="com.nucsoft.hibernate.Manager" table="manager" schema="hibernate"> <id name="managerId" column="manager_id"> <generator class="native"/> </id> <property name="managerName" column="manager_name"/> <one-to-one name="dept" class="com.nucsoft.hibernate.Department"/> </class> </hibernate-mapping>
将 Manager.hbm.xml 文件的 <one-to-one> 节点的 property-ref 属性去掉了。
(5)CRUD及需要注意的问题
<1>save
@Test public void testSave() { Department department = new Department(); department.setDeptName("dept=1"); Manager manager = new Manager(); manager.setManagerName("manager-1"); manager.setDept(department); department.setManager(manager); session.save(manager); session.save(department); }
@Test public void testSave() { Department department = new Department(); department.setDeptName("dept=2"); Manager manager = new Manager(); manager.setManagerName("manager-2"); manager.setDept(department); department.setManager(manager); session.save(department); session.save(manager); }
Hibernate: insert into hibernate.manager (manager_name) values (?) Hibernate: insert into hibernate.department (dept_name, dept_id) values (?, ?)
结论:
发现不论是先保存 department ,还是先保存 manager,都是先插入的 manager。因为 department 的主键生成方式是依赖于 manager 的。
<2>update
@Test public void testUpdate() { Department department = (Department) session.get(Department.class, 1); System.out.println(department.getManager().getManagerName()); department.getManager().setManagerName("@@"); }
Hibernate: select department0_.dept_id as dept1_0_0_, department0_.dept_name as dept2_0_0_ from hibernate.department department0_ where department0_.dept_id=? Hibernate: select manager0_.manager_id as manager1_1_1_, manager0_.manager_name as manager2_1_1_, department1_.dept_id as dept1_0_0_, department1_.dept_name as dept2_0_0_ from hibernate.manager manager0_ left outer join hibernate.department department1_ on manager0_.manager_id=department1_.dept_id where manager0_.manager_id=? manager-1 Hibernate: update hibernate.manager set manager_name=? where manager_id=?
@Test public void testUpdate() { Manager manager = (Manager) session.get(Manager.class, 1); System.out.println(manager.getDept().getDeptName()); manager.getDept().setDeptName("@@Dept"); }
Hibernate: select manager0_.manager_id as manager1_1_1_, manager0_.manager_name as manager2_1_1_, department1_.dept_id as dept1_0_0_, department1_.dept_name as dept2_0_0_ from hibernate.manager manager0_ left outer join hibernate.department department1_ on manager0_.manager_id=department1_.dept_id where manager0_.manager_id=? dept=1 Hibernate: update hibernate.department set dept_name=? where dept_id=?
<3>get
@Test public void testGet() { Department department = (Department) session.get(Department.class, 1); System.out.println(department.getManager().getClass()); System.out.println("--------------------------------------"); Manager manager = (Manager) session.get(Manager.class, 1); }
Hibernate: select department0_.dept_id as dept1_0_0_, department0_.dept_name as dept2_0_0_ from hibernate.department department0_ where department0_.dept_id=? class com.nucsoft.hibernate.Manager_$$_javassist_1 -------------------------------------- Hibernate: select manager0_.manager_id as manager1_1_1_, manager0_.manager_name as manager2_1_1_, department1_.dept_id as dept1_0_0_, department1_.dept_name as dept2_0_0_ from hibernate.manager manager0_ left outer join hibernate.department department1_ on manager0_.manager_id=department1_.dept_id where manager0_.manager_id=?
通过 department 获取到的 manager 采用的是懒加载机制。而从 manager 获取 dept ,是通过左外链接查询的。
至于原因,第一小点已经提到,本质上和基于外键的双向一对一关联一样,只不过是以主键作为了外键来使用的。
<4>delete
@Test public void testDelete() { Manager manager = (Manager) session.get(Manager.class, 1); session.delete(manager); }
Cannot delete or update a parent row: a foreign key constraint fails (`hibernate`.`department`, CONSTRAINT `FK_8hf3vewo7w3v9doungcc51wwy` FOREIGN KEY (`dept_id`) REFERENCES `manager` (`manager_id`))
若先删除 manager ,且存在和此 manager 关联的 department ,需要先删除关联的 department 记录。
三、映射 n 对 n 关联关系
1.单向的 n 对 n 关联
(1)必须使用中间表
(2)以 Category 和 Item 为例。一个分类下可以有多个商品,一个商品可以属于多个分类。以 Category 下存在 Set<Item> 集合为例来测试单向的 n 对 n 映射。
(3)创建 category、item、categories_items 表
CREATE TABLE category ( category_id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(50) ); CREATE TABLE item ( item_id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, item_name VARCHAR(50) ); CREATE TABLE categories_items ( category_id INT(11) NOT NULL, item_id INT(11) NOT NULL );
(4)通过 Intellij Idea 生成持久化类和 Entity.hbm.xml 文件
生成的持久化类和 Category.hbm.xml 和 Item.hbm.xml 文件。
public class Category { private Integer categoryId; private String categoryName; private Set<Item> items; }
public class Item { private Integer itemId; private String itemName; }
<hibernate-mapping> <class name="com.nucsoft.hibernate.Item" table="item" schema="hibernate"> <id name="itemId" column="item_id"> <generator class="native"/> </id> <property name="itemName" column="item_name"/> </class> </hibernate-mapping>
<hibernate-mapping> <class name="com.nucsoft.hibernate.Category" table="category" schema="hibernate"> <id name="categoryId" column="category_id"> <generator class="native"/> </id> <property name="categoryName" column="category_name"/> <!-- table 指中间表 --> <set name="items" table="categories_items" schema="hibernate"> <key> <!-- category 在中间表中的列名 --> <column name="category_id"/> </key> <!-- class Set 集合中持久化类的类名, column Set集合中的持久化类咋中间表的外键列的名称 --> <many-to-many not-found="ignore" class="com.nucsoft.hibernate.Item"> <column name="item_id"/> </many-to-many> </set> </class> </hibernate-mapping>
注释已经讲的很明白了。
(5)CRUD 以及需要注意的地方。
<1>save
@Test public void testSave() { Item item = new Item(); item.setItemName("item-aa"); Item item2 = new Item(); item2.setItemName("item-bb"); Category category = new Category(); category.setCategoryName("cate-1"); category.getItems().add(item); category.getItems().add(item2); session.save(item); session.save(item2); session.save(category); }
Hibernate: insert into hibernate.item (item_name) values (?) Hibernate: insert into hibernate.item (item_name) values (?) Hibernate: insert into hibernate.category (category_name) values (?) Hibernate: insert into hibernate.categories_items (category_id, item_id) values (?, ?) Hibernate: insert into hibernate.categories_items (category_id, item_id) values (?, ?)
@Test public void testSave() { Item item = new Item(); item.setItemName("item-cc"); Item item2 = new Item(); item2.setItemName("item-dd"); Category category = new Category(); category.setCategoryName("cate-2"); category.getItems().add(item); category.getItems().add(item2); session.save(category); session.save(item); session.save(item2); }
Hibernate: insert into hibernate.category (category_name) values (?) Hibernate: insert into hibernate.item (item_name) values (?) Hibernate: insert into hibernate.item (item_name) values (?) Hibernate: insert into hibernate.categories_items (category_id, item_id) values (?, ?) Hibernate: insert into hibernate.categories_items (category_id, item_id) values (?, ?)
结论:
对比发现,不论是先保存 Item 还是先保存 Category 对象,都不会多发送 UPDATE 语句,因为最终的关联关系是通过中间表进行维护的。
<2>get
@Test public void testGet() { Category category = (Category) session.get(Category.class, 1); System.out.println(category.getItems().getClass()); System.out.println(category.getItems().size()); }
Hibernate: select category0_.category_id as category1_1_0_, category0_.category_name as category2_1_0_ from hibernate.category category0_ where category0_.category_id=? class org.hibernate.collection.internal.PersistentSet Hibernate: select items0_.category_id as category1_1_1_, items0_.item_id as item2_0_1_, item1_.item_id as item1_2_0_, item1_.item_name as item2_2_0_ from hibernate.categories_items items0_ inner join hibernate.item item1_ on items0_.item_id=item1_.item_id where items0_.category_id=? 2
还是懒加载,但是查询关联的 Items 的时候,内连接关联了中间表。
<3>delete
@Test public void testDelete() { Category category = (Category) session.get(Category.class, 1); session.delete(category); }
Hibernate: select category0_.category_id as category1_1_0_, category0_.category_name as category2_1_0_ from hibernate.category category0_ where category0_.category_id=? Hibernate: delete from hibernate.categories_items where category_id=? Hibernate: delete from hibernate.category where category_id=?
删除 category 的时候,先删除的中间表,然后才删除的 category表。表明,先解除关系,然后删除。
@Test public void testDelete2() { Item item = (Item) session.get(Item.class, 4); session.delete(item); }
Cannot delete or update a parent row: a foreign key constraint fails (`hibernate`.`categories_items`, CONSTRAINT `FK_pxxtlg5rb8it8ewp5lxhss3c5` FOREIGN KEY (`item_id`) REFERENCES `item` (`item_id`))
删除 item 时,因为有关联关系的存在,且维护关联关系是由 Category 维护的,所以无法删除。
2.映射双向的 n 对 n
(1)还以 Category 和 Item 为例。Category 中拥有 Set<Item> 类型的属性, Item 中拥有 Set<Category> 类型的属性。
(2)通过 Intellij Idea 生成持久化类和 Entity.hbm.xml 文件。
对应生成的文件:
public class Category { private Integer categoryId; private String categoryName; private Set<Item> items; }
public class Item { private Integer itemId; private String itemName; private Set<Category> categories; }
<hibernate-mapping package="com.nucsoft.hibernate" schema="hibernate"> <class name="Item" table="item"> <id name="itemId" column="item_id"> <generator class="native"/> </id> <property name="itemName" column="item_name"/> <set name="categories" inverse="true" table="categories_items"> <key> <column name="item_id"/> </key> <!-- 交叉对应 --> <many-to-many not-found="ignore" class="Category"> <column name="category_id"/> </many-to-many> </set> </class> </hibernate-mapping>
<hibernate-mapping package="com.nucsoft.hibernate" schema="hibernate"> <class name="Category" table="category"> <id name="categoryId" column="category_id"> <generator class="native"/> </id> <property name="categoryName" column="category_name"/> <set name="items" table="categories_items"> <key> <column name="category_id"/> </key> <!-- 交叉对应 --> <many-to-many not-found="ignore" class="Item"> <column name="item_id"/> </many-to-many> </set> </class> </hibernate-mapping>
(3)注意:
- 需要两端都使用集合属性
- 必须使用中间表
- 两边都需要指定中间表表名以及在中间表中外键列的列名。
- 必须把其中一端 的 inverse 属性设置为 true
四、总结
介绍了Hibernate如何映射 单双向的多对一,基于外键、基于主键的一对一,以及单双向的多对多的关联关系。包括在 Intellij Idea下如何操作,以及各个类型映射下的 CRUR 以及需要注意的地方。
没有介绍 Hibernate 是如何映射继承关系,是因为在真实的生产环境下还没有遇到这种情况,这里不做说明。
文章篇幅较长,所以摘录了一个附录出来,以便查询方便。http://www.cnblogs.com/solverpeng/p/5953536.html
写文章不易,若转载,请标明出处。
注意:每次通过 Intellij Idea 的 Database Schema 生成 Entity.hbm.xml 和持久化类的时候,都会覆盖数据库连接的 username 和 password 。需要重新添加。