在领域模型中,类与类之间最普遍的关系就是关联关系。在 UML 中,关联是有方向的。以 Customer 和 Order 为例:一个用户能发出多个订单, 而一个订单只能属于一个客户。从 Order 到 Customer 的关联是多对一关联; 而从 Customer 到 Order 是一对多关联。
单向n-1的关联关系
单向 n-1 关联只需从 n 的一端可以访问 1 的一端。
域模型:从 Order 到 Customer 的多对一单向关联需要在Order 类中定义一个 Customer 属性,而在 Customer 类中无需定义存放 Order 对象的集合属性。
关系数据模型: ORDERS 表中的 CUSTOMER_ID 参照 CUSTOMER 表的主键。(外键)
Hibernate 使用
例如,在Customer和Order的例子中,首先创建两个类文件:
生成hibernate映射文件:
Customer.hbm.xml
Order.hbm.xml
先随便运行一个程序,来生成数据库表:
customers表
orders表
单向n-1的save操作:
运行程序,可以成功插入记录,并且在控制台只会打印三条insert语句:
但是,如果注释掉倒数4,5,6行的代码,而使用最后的三行代码,即先保存order1和order2,再保存customer,同样也可以成功插入,但是除了会输出三行insert语句,还会输出两行update语句。如下图:
这是因为在先插入order记录时,无法确定外键值customer_id,只能先置为null,所以只能等customer记录插入后,再额外发送 UPDATE 语句去更新customer_id。所以,建议先插入1的那一端,即customer,后插入n的这一端,即order。
单向n-1的get操作:
在n-1的get操作中,使用了懒加载机制。如果查询的是order对象,则默认情况下不会立即查找对应的customer对象,而只有等到需要使用这个customer对象时,才会发送select语句查询该customer对象。那么当然,如果在使用该对象之前,session被关闭了,也会抛出懒加载异常。
单向n-1的update操作:
单向n-1的delete操作:
在不设定级联关系的情况下,且 1 这一端的对象有 n 的对象在引用,则不能直接删除 1 这一端的对象。我们可以在customer映射文件的set节点中设置级联属性为级联删除,就可以直接删除1这一端的对象:
除了级联删除之外,还有其他的级联属性,如下图所示:
但是在开发时并不建议设定级联属性,而建议使用手工的方式来处理。
双向1-n的关联关系
双向 1-n 与双向 n-1 是完全相同的两种情形。
双向 1-n 需要在1的一端可以访问n的一端,反之亦然。
域模型:从Order到Customer的多对一双向关联需要在Order类中定义一个Customer属性,而在Customer类中需定义存放Order对象的集合属性。
关系数据模型: ORDERS表中的CUSTOMER_ID参照CUSTOMER表的主键。(外键)
几个注意点:
- 当 Session从数据库中加载Java集合时,创建的是Hibernate内置集合类的实例,因此在持久化类中定义集合属性时必须把属性声明为Java 接口类型,例如应该声明为Set而不是HashSet。Hibernate 的内置集合类具有集合代理功能,支持延迟检索策略。类似于在单向n-1关系的get操作,如果在双向1-n的get操作中获取了customer对象,如果不使用它存放order的集合,那么这个集合就不会被加载,只有使用到时才会加载。
在customer类中定义集合属性时,通常把它初始化为集合实现类的一个实例,这样可以提高程序的健壮性,避免应用程序访问取值为null的集合的方法抛出NullPointerException。
Hibernate 使用
元素来映射set类型的属性。下面我们仍以customer和order的例子来测试,首先创建两个类: 123456789101112131415161718192021222324public class Customer {private Integer customerId;private String customerName;/** 1. 声明集合类型时, 需使用接口类型, 因为 hibernate 在获取* 集合类型时, 返回的是 Hibernate 内置的集合类型, 而不是 JavaSE 一个标准的集合实现.* 2. 需要把集合进行初始化, 可以防止发生空指针异常*/private Set<Order> orders = new HashSet<>();//getters and setters}public class Order {private Integer orderId;private String orderName;private Customer customer;//getters and setters}
生成Hibernate映射文件:
Order.hbm.xml(和单向n-1相同)
Customer.hbm.xml
生成的数据库表的结构和单向n-1中相同,现在来测试各种方法:
双向1-n的save操作:
当我们使用倒数第4,5,6行代码进行插入,即先保存customer,再保存order1和order2,这不同于单向n-1中的操作,虽然插入都是成功的,但是此处除了会打印三条insert语句,还会打印两条update语句:
这是因为, 由于是双向的关联关系,所以 1 的一端和 n 的一端都需要维护关联关系。当先插入customer对象后,customer中的set集合里面的order的id也还未知,会先被置为null,所以当order1和order2被插入后,会多出两条update语句。那么可以推测出,如果注释掉倒数第4,5,6行代码,而执行最后三行代码,那么除了有三条insert语句,还会打印出4条update语句,因为order1和order2先各维护一次,customer会再维护两次。如下图:
那么,如果我们不希望两端都维护关联关系,该怎么办呢?
解决办法是,在hibernate的配置文件中可以通过设置inverse属性来决定是由双向关联的哪一方来维护表和表之间的关系。inverse = false的为主动方,inverse = true 的为被动方。由主动方负责维护关 联关系。在没有设置inverse属性的情况下,默认父子两边都维护父子关系。
在双向 1-n 关系中,将 n 方设为主控方将有助于性能改善,而如果将 1 方设为主控方会额外多出update语句。这好比如果要国家元首记住全国人民的名字不太现实,但要让全国人民知道国家元首,就容易得多。
例如,现在我们在Customer.hbm.xml映射文件中设置set节点为:
然后在test方法中先插入customer,再插入order1和order2,则只会打印三条insert语句,而不会打印update语句:
双向1-n的get操作:
与单向n-1的get操作类似,在双向1-n的get操作中,如果先加载了customer对象,在使用它的orders集合之前,是不会加载orders集合的,这使用了懒加载机制,那么同样,也有可能抛出懒加载异常。
双向1-n的update操作:
可以通过1这一端的customer来更新n那一端的order。
双向1-n的delete操作:
同单向n-1中一样,在不设定级联关系的情况下,且 1 这一端的对象有 n 的对象在引用,则不能直接删除 1 这一端的对象。
此外,我们还可以在customer映射文件的set节点中设置order-by属性, 在查询时对集合中的元素进行排序,order-by 中使用的是表的字段名, 而不是持久化类的属性名。例如,对orders集合根据orderName进行降序排列:
最后补充一下,关于双向1-n的两个类的映射文件中,属性的对应关系,如下图所示: