映射组成关系
建立域模型和关系型数据库模型有着不同的出发点
域模型:由程序代码组成,通过细化持久化类的粒度可以提高代码的可重用性,简化编程。
在没有数据冗余的情况下,尽可能的减少表的数量,简化表之间的参照关系,以便提高数据的访问速度。
映射组成关系
Hibernate把持久化类的属性分为两种:
- 值(value)类型:没有OID,不能被单独持久化,生命周期依赖于所属的持久化类的对象生命周期。
- 实体(entity)类型:有OID,可以被单独持久化,有独立的生命周期。
示例:
首先创建被参照对象
public class Pay {
private int monthlyPay;
private int yearPay;
private int vocationWithPay;
//Getter and setter.
}
参照对象
public class Worker {
private Integer id;
private String name;
private Pay pay;
//Getter and setter.
}
配置文件
<hibernate-mapping>
<class name="com.hibernate.session.Worker" table="worker">
<id name="id" type="java.lang.Integer">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="java.lang.String">
<column name="NAME"/>
</property>
<component name="pay" class="com.hibernate.session.Pay">
<property name="monthlyPay" column="MONTHLY_PAY"></property>
<property name="yearPay" column="YEAR_PAY"></property>
<property name="vocationWithPay" column="VOCATION_WITH_PAY"></property>
</component>
</class>
</hibernate-mapping>
无法直接使用<property>映射对象属性。需要使用<component>标签映射组成关系,该元素的表名pay属性是Worker类的一个组成部分。在Hibernate中称为组件。
class:设定关系属性的类型。
parent:指定属性所属的整体类。其中的name指定整体类在组件类中的属性名。
运行测试类
Worker worker = new Worker();
Pay pay = new Pay();
pay.setMonthlyPay(1000);
pay.setYearPay(80000);
pay.setVocationWithPay(5);
worker.setName("HFR");
worker.setPay(pay);
session.save(worker);
运行成功后我们发现,数据成功的插入到了数据库。
一对多的关联关系
以Customer和Order为例,一个用户能够发出多个订单,而一个订单只能属于一个用户。这就是一个一对多的关系。
单向n-1
单向n-1关联,只需要从n的一端就可以访问1的一端。
域模型:从Order到Customer的多对一单向关联需要在Order类中定义一个Customer属性,而在Customer类中无需定义存放Order对象的属性集合。
关系型数据库模型:ORDERS表中的CUSTOMER_ID参照CUSTOMER表的主键。
Hibernate示例:
首先创建Customer对象
public class Customer {
private Integer customerId;
private String customerName;
//Getter and setter
}
之后是Order类型
public class Order {
private Integer orderId;
private String orderName;
private Customer customer;
//Getter and setter
}
编写hbm.xml文件
<hibernate-mapping>
<class name="com.hibernate.n21.Customer" table="customer">
<id name="customerId" type="java.lang.Integer">
<column name="COSTUMER_ID"/>
<generator class="native"/>
</id>
<property name="customerName" type="java.lang.String">
<column name="CUSTOMER_NAME"/>
</property>
</class>
</hibernate-mapping>
<hibernate-mapping package="com.hibernate.n21">
<class name="Order" table="orders">
<id name="orderId" type="java.lang.Integer">
<column name="ORDER_ID"/>
<generator class="native"/>
</id>
<property name="orderName" type="java.lang.String">
<column name="ORDER_NAME"/>
</property>
<many-to-one name="customer" class="Customer" column="CUSTOMER_ID" fetch="join">
</many-to-one>
</class>
</hibernate-mapping>
<many-to-one>标签解释:
使用<many-to-one>对应多对一的关联关系。
name:多的一端关联的一的一端的属性名。
class:一的一端的属性对应的类型。
column:一那一端在多的一端对应的数据表中的外键的名称。
在hibernate.cfg.xml中加入配置
<mapping resource="com/hibernate/n21/Customer.hbm.xml"/>
<mapping resource="com/hibernate/n21/Order.hbm.xml"/>
测试类,进行保存数据操作
Customer customer = new Customer();
customer.setCustomerName("HFR");
Order order1 = new Order();
order1.setOrderName("order1");
Order order2 = new Order();
order2.setOrderName("order2");
order1.setCustomer(customer);
order2.setCustomer(customer);
session.save(customer);
session.save(order1);
session.save(order2);
几个需要注意的点
先插入1的一端,再插入多的一端。
如果先插入n的一端,再插入1的一端,则会多出update语句。由于在插入多的一端时还无法确定外键值,只有在插入1的一端之后才能确定外键值。所以会多出update。Hibernate会在插入时把缺少的外键值设为null,等插入1的一端后有了外键值,再去执行update。这是多出update原因的根本原因。所以推荐先插入1的一端,再插入多的一端。查询功能:
若只查询多的一端的对象,则不会将其关联的1的对象查到。
在需要使用到关联的对象值,才发送对应的SQL语句。
由多的一端导航到1的一端时,若此时session关闭,一定会发生懒加载异常(LazyInitException)。
获取Order对象及关联的Customer对象时,其关联的Customer对象是一个代理对象。直到需要用的时候才会对代理对象进行初始化。这就是所说的懒加载。级联问题
在不设定级别关系的情况下,不能直接删除1的一端的对象且1这一端的对象有多个对象在引用则不能删除。
双向1-n
双向1-n和双向n-1是两种完全相同的情形。
双向1-n需要在1的一端能够访问n的一端。反之亦然。
域模型:从Order到Customer的多对一双向关联需要在Order类定义一个Customer属性,而在Customer类中需要定义存放Order对象的属性集合。
完善Customer类
public class Customer {
private Integer customerId;
private String customerName;
private Set<Order>orders = new HashSet<>();
//Getter and setter
}
完善hbm.xml
<hibernate-mapping>
<class name="com.hibernate.n21both.Customer" table="customer">
<id name="customerId" type="java.lang.Integer">
<column name="COSTUMER_ID"/>
<generator class="native"/>
</id>
<property name="customerName" type="java.lang.String">
<column name="CUSTOMER_NAME"/>
</property>
<!-- table:set中元素对应的记录放在那一个数据表中,该列的值需要和n对1中n的表的名字一致 -->
<set name="orders" table="orders">
<!-- n的表中的外键名 -->
<key column="CUSTOMER_ID"></key>
<!-- 指定映射类型 -->
<one-to-many class="com.hibernate.n21both.Order"/>
</set>
</class>
</hibernate-mapping>
测试类
Customer customer = new Customer();
customer.setCustomerName("HFR");
Order order1 = new Order();
order1.setOrderName("order1");
Order order2 = new Order();
order2.setOrderName("order2");
customer.getOrders().add(order1);
customer.getOrders().add(order2);
session.save(customer);
session.save(order1);
session.save(order2);
执行后我们发现,执行了3条insert语句,2条update语句。为什么会多出2条update语句呢?
因为1的一端和n的一端都维护关联关系,所以多出了两条update。如果希望不执行多余的update,则需要1的一端不维护关联关系。
可以使用set集合的inverse属性,让1的一端放弃维护关联关系。
下面来详细解释一下<set>元素的inverse属性:
在hibernate中通过对inverse属性来决定由双向关联的哪一方维护表与表之间的关系,inverse为false的则为主动方。inverse为true的则为被动方。由主动方负责维护关联关系。
在没有设置inverse为true的情况下,父子两方都维护关联关系。
在1-n的关系中,将n的一方设为主动方有助于性能的改善。就像如果要求国家元首把全国人民的名字都记住,这是不现实的,但要全国人民知道国家元首的名字则会很容易。
在1-n的关系中,将1设为主动方:会额外多出很多update语句。插入数据时无法同时插入外键列,因而无法向外键列添加非空约束。
可以在1的一端的set结点指定inverse为true,使1的一端放弃维护关联关系。建议先插入1的一端,后插入n的一端,这样不会多出update语句。
Customer customer = session.get(Customer.class, 3);
System.out.println(customer);
对n的一端的集合使用延迟加载。返回的n的一端的集合是Hibernate内置的集合类型。该类型具有延迟加载和存放外部对象的功能。
我们输出customer中orders的类型,我们发现是这种类型
class org.hibernate.collection.internal.PersistentSet
因此声明集合类型时需要使用接口类型。因为Hibernate在获取集合类型时返回的是HIbernate内置的集合类型,而不是JavaSE的标准的实现。所以,需要在声明orders的时候将其声明为Set类型。而不能是HashSet类型,因为HashSet不是PersistentSet的子类。
private Set<Order>orders = new HashSet<>();
可能会抛出LazyLoadingException。会在sesssion.close()的时候抛出。
同样的,由于是懒加载,只会在需要使用集合中元素的时候进行初始化。
一对一的关联关系
基于外键的一对一映射查询,外键可以放在任意一端,在需要存放外键的一端添加many-to-one元素,为many-to-one元素添加unique="true"的属性。另一端使用one-to-one元素,该元素使用property-ref属性指定被关联实体主键以外的字段作为关联字段。
在对应的表中已经有了外键,当前持久化类使用one-to-one进行映射。
首先创建两个类Manger和Department,它们之间是一对一的对应关系。
Manger
public class Manager {
private Integer mgrId;
private String mgrName;
private Department dept;
//Getter and setter
}
Department
public class Department {
private Integer deptId;
private String deptName;
private Manager manager;
//Getter and setter
}
hbm.xml文件
<hibernate-mapping package="com.hibernate.one2one">
<class name="Manager" table="manager">
<id name="mgrId" type="java.lang.Integer">
<column name="MGR_ID"/>
<generator class="native"/>
</id>
<property name="mgrName" type="java.lang.String">
<column name="MGR_NAME"/>
</property>
<one-to-one name="dept" class="Department"></one-to-one>
</class>
</hibernate-mapping>
<hibernate-mapping package="com.hibernate.one2one">
<class name="Department" table="department">
<id name="deptId" type="java.lang.Integer">
<column name="DEPT_ID"/>
<generator class="native"/>
</id>
<property name="deptName" type="java.lang.String">
<column name="DEPT_NAME"/>
</property>
<many-to-one name="manager" class="Manager" column="MGR_ID" unique="true"></many-to-one>
</class>
</hibernate-mapping>
运行程序之后,将会在数据库中创建相关的表。
之后我们要执行的是保存操作。
Department department = new Department();
department.setDeptName("Dept-A");
Manager manager = new Manager();
manager.setMgrName("MGR-A");
department.setManager(manager);
manager.setDept(department);
session.save(manager);
session.save(department);
建议先保存没有外键列的对象,这样会减少update语句。
Department dept = session.get(Department.class, 12);
System.out.println(dept.getDeptName());
默认情况下对关联属性使用懒加载。所以会出现懒加载异常的问题。
当我们需要懒加载一个对象的时候
Manager mgr = dept.getManager();
System.out.println(mgr.getMgrName());
虽然顺利的获取了对象,但是当我们查看SQL语句的时候发现了有些地方不对
查询Manger对象的连接条件应该是Manger表的MGR_ID等于DEPARTMENT的MGR_ID。这里确是Manger的MGR_ID等于DEPARTMENT的DEPT_ID。
<one-to-one name="dept" class="Department" property-ref="manager"></one-to-one>
由于我们使用了property-ref。此时连接查询的时候我们会使用dept的manager列对应的属性作为连接条件。没使用该属性的情况我们使用dept的主键作为连接条件。
没有外键的一端使用one-to-one元素,该元素使用property-ref属性指定关联实体主键以外的字段作为关联字段。
在查询没有外键的实体对象时,使用的是左外连接查询,一并查询出关联的对象并进行初始化。
基于主键映射的1对1关系
基于主键的映射策略,指一端的主键生成器使用foreign策略,表明根据对方的主键来生成自己的主键,自己并不需要独立生成主键。<param>属性指定使用当前持久化类的主键作为“对方”。
采用foreign主键生成器策略的一端使用one-to-one元素映射关联属性,one-to-one元素还应该增加constrained=“true“属性,另一端增加one-to-one元素映射关联属性。相当于添加外键约束。
示例
依然使用上述的类,只是配置文件(hbm.xml)有所不同。
<hibernate-mapping package="com.hibernate.primary">
<class name="Manager" table="managers">
<id name="mgrId" type="java.lang.Integer">
<column name="MGR_ID"/>
<generator class="native"/>
</id>
<property name="mgrName" type="java.lang.String">
<column name="MGR_NAME"/>
</property>
<one-to-one name="dept" class="Department"></one-to-one>
</class>
</hibernate-mapping>
<hibernate-mapping package="com.hibernate.primary">
<class name="Department" table="departments">
<id name="deptId" type="java.lang.Integer">
<column name="DEPT_ID"/>
<generator class="foreign">
<param name="property">manager</param>
</generator>
</id>
<property name="deptName" type="java.lang.String">
<column name="DEPT_NAME"/>
</property>
<one-to-one name="manager" class="Manager" constrained="true"></one-to-one>
</class>
</hibernate-mapping>
插入操作
Department department = new Department();
department.setDeptName("Dept-A");
Manager manager = new Manager();
manager.setMgrName("MGR-A");
department.setManager(manager);
manager.setDept(department);
session.save(manager);
session.save(department);
无论先插入那一个都不会有多余的update操作。
主键列不能设置为null,因此必须先插入有效的主键。这也是不会产生多余的update的原因。
多对多的关联关系
域模型:
关系数据模型:
n-n的关联必须使用中间表
与1-n映射类似,必须为set集合元素添加key子元素。指定CATEGORIES_ITEMS表中参照CATEGORIES表的外键为CATEGORY_ID,与1-n关联映射不同的是,建立n-n关联时,集合中所用的元素为many-to-many。many-to-many的子元素使用的是class属性,指定items集合中存放的是Item对象。column属性指定CATEGORIES_ITEMS表中参照Items表中的外键为ITEM_ID。
示例:
Category类
public class Category {
private Integer id;
private String name;
private Set<Item> items = new HashSet<>();
//Getter and setter
}
Item类
public class Item {
private Integer id;
private String name;
//Getter and setter
}
配置文件Item.hbm.xml
<hibernate-mapping package="com.hibernate.n2n">
<class name="Item" table="items">
<id name="id" type="java.lang.Integer">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="java.lang.String">
<column name="NAME"/>
</property>
</class>
</hibernate-mapping>
category.hbm.xml
<hibernate-mapping package="com.hibernate.n2n">
<class name="Category" table="category">
<id name="id" type="java.lang.Integer">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="java.lang.String">
<column name="NAME"/>
</property>
<!-- table指定中间表 -->
<set name="items" table="category_item">
<key>
<column name="Category_Id"/>
</key>
<!-- 使用many-to-many指定多对多关系,column执行Set在持久化类中在中间表的外键类名称 -->
<many-to-many class="Item" column="Item_Id"/>
</set>
</class>
</hibernate-mapping>
测试类
Category category1 = new Category();
category1.setName("Category 1");
Category category2 = new Category();
category2.setName("Category 2");
Item item1 = new Item();
item1.setName("item1");
Item item2 = new Item();
item2.setName("item2");
category1.getItems().add(item1);
category1.getItems().add(item2);
category2.getItems().add(item1);
category2.getItems().add(item2);
session.save(category1);
session.save(category2);
session.save(item1);
session.save(item2);
双向n-n关联
双向n-n关联需要两端都使用集合属性。
双向n-n关联必须使用连接表。
集合属性应增加key子元素用以映射外键列,集合元素里还应该增加many-to-many子元素关联实体类。
在双向n-n关联的两边都需要指定连接表的表名和外键列的列名,两个集合元素set的table元素的值必须指定,而且必须相同。set元素的两个子元素,key和many-to-many必须指定column属性。其中key和many-to-many分别指定本持久化类和关联类在连接表中的外键列的列名。因此两边的key和many-to-many的column属性交叉相同。
对于双向n-n关联,必须把一段的inverse设为true。否则两端都维护关联关系可能会造成主键冲突。
基于双向n-n关系改写上述操作
Item类
public class Item {
private Integer id;
private String name;
private Set<Category>categories = new HashSet<>();
//Getter and setter.
}
Item.hbm.xml
<hibernate-mapping package="com.hibernate.n2n">
<class name="Item" table="items">
<id name="id" type="java.lang.Integer">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="java.lang.String">
<column name="NAME"/>
</property>
<set name="categories" table="category_item" inverse="true">
<key column="Item_Id"></key>
<many-to-many class="Category" column="Category_Id"></many-to-many>
</set>
</class>
</hibernate-mapping>
测试类
Category category1 = new Category();
category1.setName("Category 1");
Category category2 = new Category();
category2.setName("Category 2");
Item item1 = new Item();
item1.setName("item1");
Item item2 = new Item();
item2.setName("item2");
category1.getItems().add(item1);
category1.getItems().add(item2);
category2.getItems().add(item1);
category2.getItems().add(item2);
item1.getCategories().add(category1);
item1.getCategories().add(category2);
item2.getCategories().add(category1);
item2.getCategories().add(category2);
session.save(category1);
session.save(category2);
session.save(item1);
session.save(item2);