Hibernate关联关系
一、对象-关系 映射基础
1.对象间的基本关系
首先阐述一下对象之间的基本关系,如果想深入学习话,请查看UML的相关资料。对象具有的四种基本关系:
关联关系:关联关系在设计模式中是被提倡优先使用于继承关系的。关联关系就是将一个对象做为别一个对象的成员,是一种包含的关系。
依赖关系:对与对象之间的方法的调用,不存在包含的关系。依赖关系总是单向的 。可以简单的理解,就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性、临时性,是非常弱的,但是B类的变化会影响到A;比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖;表现在代码层面,为类B作为参数被类A在某个method方法中使用。
聚集关系:这个关系比较有趣,比如人的手和身体。如果身体不存在了,手也就不存在了。是一种整个与部分的关系。
一般关系:就是继承关系。
详细的关于对象关系的讲解见 http://justsee.iteye.com/blog/808799
2.持久化类的属性及访问方法
首先回顾一下持久化,我们知道持久化层是从业务逻辑层中分离出来的专门用于数据库操作的这些部分。持久化层中的持久化类,便是我们之前早已学习的domain类。
1).持久化类的访问者有两个,一是JAVA应用程序,二是hibernate。
写:Java应用程序通过setter设置持久化对象的属性,hibernate通过getter获取持久化对象的属性并生成相应的SQL语句对表格进行操作。
读:hibernate通过setter设置持久化对象的属性,Java应用程序通过getter获取持久化对象的属性。
2).基本数据类型和包装类型
关联对象的属性与表格的字段是通过property元素节点设置的: <property name="gender" column="gender" type="integer" />
基本的type是hibernate的类型,我们在持久化类中定义的gender属性为int。定义为int类型会有什么弊端?比如,我们有个学生成绩表。如果某个学生没有参加某一学科的考试,但我们却使用了int类型,它的默认值为0,当查看学生成绩时,他到底是考了0分还是没有考试?所以最好将持久化类中的gender属性定义为Integer,它的默认值为null。查询成绩时看到的是null,那么他肯定是没参加考试哦!(注意:数据库中的对应字段应该为字符型)
3).hibernate访问持久化类属性的策略
Hibernate通过name指定的值访问持久化对象。Hibernate通过name值,反射持久化对象的对方法。比如,name的值为gender。Hibernate会直接反射持久化对象的getGender和setGender方法。所以我们必须保证持久化对象中有对应的方法。这是因为property有一个access属性,它的默认值为property。
如果指定access的值为field,则hibernate直接根据name值反射持久化对象的属性。此时,我们必须保证持久化对象中有对应的属性。
4).在持久化类的方法中加入程序逻辑
通过3)我们知道,如果access的值为property,hibernate直接反射持久化对象的方法。在这个方法中我们就可以加入程序逻辑。
比如Customer类中有firstname和lastname两个属性。但我们只想让hibernate通过getName方法获得一个firstname+lastname的字符串,此时我们就可以在getName方法中将firstname与lastname两个属性值合并为一个中间使用 “.”连接的字符串返回。
使用hibernate获取数据表中的数据时,hibernate会调用持久化对象的setName方法。我们在这个方法中将传递进来的参数使用“.”分隔,然后分别设置到firestname和lastname属性中。
5).hibernate的hql语句
我们在使用JDBC、DBUtil时使用的都是SQL语句。但hibernate比较特殊,它使用的是自己的一套东西叫hql语句。
比如我们调用session.find方法,传递的hql语句为:"from customer as c where c.name='itcast'"
其中的customer指向持久化对象的映射文件,name指向持久化对象的映射文件中的property元素的name属性。此时需要注意access属性的值。
6).设置派生属性
property元素中,有一个formula属性。它的值是一个sql表达式,hibernate将根据此表达式计算的值设置到持久化对象的属性上。比如,我们要统计订单表中的总计:
<property name="totalprice" formula="(select sum(o.PRICE) from ORDERS o where o.CUSTOMER_ID=ID)" />
十分方便!
7).控制insert和update属性
映射属性
作用
<property>: insert属性
若为false,在insert语句中不包含该字段,该字段永远不能被插入。默认值true。
<property>: update属性
若为false,update语句不包含该字段,该字段永远不能被更新。默认值为true。
<class>:mutable属性
若为false,等价于所有的<property>元素的update属性为false,整个实例不能被更新。默认为true。
<class>:dynamic-insert属性
若为true,等价于所有的<property>元素的dynamic-insert为true,保存一个对象时,动态生成insert语句,语句中仅包含取值不为null的字段。默认false。
<class>:dynamic-update属性
若为true,等价于所有的<property>元素的dynamic-update为true,更新一个对象时,动态生成update语句,语句中仅包含取值被改变的字段。默认false。
8).设置类的包名
在映射文件中包含了多个类,而这些类又在同一个包中。此时我们可以不在class属性的name属性中指定类的完整名称。首先我们在hibernate-mapping元素中添加一个package属性,之后的所有class子元素的name属性直接指定类名即可。
<hibernate-mapping package="cn.itcast.cc.hibernate">
二、映射对象标识符
1.关系数据库中的主键
主键不能为null、唯一且永远不会改变。
MySQL中的自动增长主键,将字段类型设置为 primary key auto_increment。
SQLServer中的自动增长主键,将字段类型设置为 primary key identity。
Oracle中的自动增长主键,需要自定义序列:create sequence seq_customer increment by 1 start with 1。
2.JAVA对象的“主键”、Hibernate中用对象表示符(OID)来区分对象
就是对象的地址,使用==与equal()比较两个对象是否相同。
Hibernate中使用OID来维护对象与数据记录的对应关系。
3.在hibernate中配置OID对应关系与主键生成器
<id name="id" type="long" column="ID">
<generator class="increment" />
</id>
4.主键生成策略
表示符生成器描述
Increment
适用于代理主键。由hibernate自动以递增的方式生成表识符,每次增量为1。即为带走加一,不依赖于底层的数据库,但多线程的时候会遇到问题。
Identity
适用于代理主键。由底层数据库生成表识符,条件是数据库支持自动增长数据类型。(Mysql)
Sequence
适用于代理主键。Hibernate根据底层数据库序列生成标识符,条件是数据库支持序列。(Oracle)
Hilo
适用于代理主键。Hibernate根据hign/low算法生成标识符。Hibernate把特定表的字段作为“hign”值。默认情况下,采用hibernate_unique_key表的next_hi字段。
Native
适用于代理主键。根据底层数据库对自动生成表示符的能力来选择identity、sequence、hilo。
Uuid.hex
适用于代理主键。Hibernate采用128位的UUID算法来生成标识符。该算法能够在网络环境中生成唯一的字符串标识符,这种策略并不流行,因为字符串类型的主键比整数类型的主键占用更多的数据库空间。
assigned
适用于自然主键。由java程序负责生成标识符,不能把setID()方法声明为Private的。尽量避免使用自然主键。
1). Hibernate中的increment
<id name="id" type="long" column="ID">
<generator class="increment"/>
</id>
适用范围:
1.由于不依赖与底层数据库,适合所有的数据库系统。
2.单个进程访问同一个数据库的场合,集群环境下不推荐适用。
3.OID必须为long、int或short类型,如果把OID定义为byte类型,抛异常。
4.在每次插入记录时,hibernate都会先调用“select max(id) from table;”返回最大的数据表中最大的id值,然后+1设置为新记录的id。使用increment会产生多进程访问的安全问题。所以在使用不依赖与底层数据库的主键时要注意这个问题。
2). Hibernate中的identity
<id name="id" type="long" column="ID">
<generator class="identity"/>
</id>
由底层数据库生成标识符,需要把字段定义成自增型。因为由数据库底层生成标识符,所以它是先在数据库中计算id,然后再返回id。这与increment不同,所以要使用identity防止多进程访问的安全。
3). Hibernate中的sequence
<id name="id" type="long" column="ID">
<generator class="sequence">
<param name="sequence">tester_id_seq</param>
</generator>
</id>
适用范围:
底层数据库要支持序列,Oracle DB2 SAP等。
OID必须为long、int或short类型。
4). Hibernate中的hilo
<id name="id" type="long" column="ID">
<generator class="hilo">
<param name="table">hi_value</param>
<param name="column">next_value</param>
<param name="max_lo">100</param>
</generator></id>
使用范围:
该机制不依赖于地层数据库,因此适用于所有的数据库系统。
OID必须为long、int、short类型,如果为byte类型的话,会抛出异常。
5). Hibernate中的native
<id name="id" type="native" column="ID">
<generator class="native" />
</id>
适用范围:
该类型能根据底层数据库系统的类型,自动选择合适的标识符生成器,因此很适合于跨数据库的平台,即在同一个应用中需要连接多种数据库系统的场合。
OID与以上类同。
6). Hibernate中的自然主键
1).单个自然主键
<id name="id" column="NAME" type="string">
<generator class="assigned" />
</id>
自然主键:把具有业务含义的字段作为主键叫做自然主键,由应用程序为id属性赋值。
2).复合主键
<composite-id>
<key-property name="name" column="NAME" type="string">
<key-property name="companyId" column="COMPANY_ID" type="long">
</composite-id>
使用复合主键的持久化类需要实现serializable接口和覆盖equals()、hashCode()方法。
<composite-id name="costomerid" class="mypack.CustomerId">
<key-property name="name" column="NAME" type="string">
<key-property name="companyId" column="COMPANY_ID" type="long">
</composite-id>
可以使用customId属性来设置连联合主键。
三、映射一对多关联关系
1.多对一单向关联关系
我们使用订单表(orders)与客户表(customs)。订单对象(Order)中包含一个客户对象(Custom),这是对象的多对一关系(一个客户可能有多个订单)。
我们需要向Order的映射文件中添加一个many-to-one元素:
<many-to-one name="customs" column="cid" class="Custom" cascade="save-update" outer-join="true"/>
many-to-one:
- name:设定待映射的持久化类的属性名称。
- column:设定和持久化类的属性对应的表的外键。
- class:设定持久化类的属性的类型。
- not-null:是否允许为空。
测试代码:
private static void manyToOne(){ //获取session相当于获取了一个连接 Session session = sefac.openSession(); Transaction tra = session.beginTransaction(); //创建一个Customer对象 Customer customer = new Customer(); customer.set...(...); //创建一个Order对象 Order order = new Order(); order.set...(...); order.setCustomer(customer); //保存Order到数据库,注意此处没有保存cutomer对象哦! session.save(order); //提交事件 tra.commit(); //关闭会话 session.close(); }
代码中没有保存customer对象,但因为在many-to-one元素中添加了“cascade="save-update"”(级联添加)属性,所 以在保存order时,hibernate会自动保存customer对象。如果没有设置“cascade="save-update"”属性,则会抛异 常:“org.hibernate.PropertyValueException: not-null property references a null or ransient value: cn.itcast.cc.hibernate.persistence.Order.customer”。
outer-join属性:有3个值,分别是true,false,auto,默认是auto。
true:
表示使用外连接抓取关联的内容,这里的意思是当使用load(Order.class,"id")时,Hibernate只生成一条SQL语句。
false:表示不使用外连接抓取关联的内容,当load(OrderLineItem.class,"id")时。这样的好处是可以设置延迟加载。
auto:具体是ture还是false看hibernate.cfg.xml中的配置
2. 一对多双向关联关系
什么是双向关联呢?上边的是单向,我们只设置了order的customer属性。我们再设置customer的orders属性,便形成了双向关联。我们需要在customer中添加一个orders属性,它的类型为set,set是一个集合并且不可包含重复信息,这样可以避免以后发生重复记录的错误。 在类被实例化时便将orders初始化为HashSet,这样可以避免空指针异常!
1).级联添加:
我们有设置order的映射文件的many-to-one,在此我们也必须设置customer的set元素:
<set name="orders" cascade="save-update" lazy="false">
<key column="cid" />
<one-to-many class="Order"/>
</set>
name:设定待映射持久化类的属性名。
cascade:设定级联操作的程度。
key子属性:设定与所关联的持久化类对应的标的外键。
one-to-many子属性:设定所关联的持久化类。
cascade属性值
none
忽略关联对象,默认值
save-update
保存或更新当前对象时,级联保存关联的临时对象,更新关联的游离对象。
delete
删除对象时,级联删除关联的对象。
all
包含save-update和delete的行为。
delete-orphan
删除所有和当前对象解除关联关系的对象。
all-delete-orphan
包含all和delete-orphan的行为。
添加代码:
customer.getOrders().add(order);
此时我们只保存customer或order对象,hibernate都可以将两个对象的数据保存到对应的表中。
注意:如果映射文件没有设置“cascade="save-update"”属性,则保存失败并抛异常!
2).级联删除:
我们调用session.delete(customer);方法时会一同删除customer对象的orders吗?不会!如果我们希望能够删除,我们必须设置Set元素的属性:cascade="delete"。
那如果我删除一个order,能自动删除对应的customer吗?你想可以吗?这本身就不符合业务逻辑!
3).Set集合的inverse属性:
这个属性十分重要,涉及到hibernate缓存监控技术。Inverse属性使得,如果持久化对象一旦发生改变就自动更新数据表中的记录。(持久化对象后面会有介绍)通过load、 save、update返回或设置的对象都是持久化对象,这些对象一旦发生改变,比如set了一个新值,hibernate就会自动将新值更新到数据库中。当调用了session.close方法是,这些持久化对象就不存在了!
Inverse=”true”的表示两个实体的关系由对方去维护。
结论:
1.在映射一对多的双向关联关系时,应该在one方把inverse属性设为true,这可以提高性能。
如果把一的一方Customer作为主控方,多的一方Order因为不知道Customer的id是多少,所以必须等Customer和Order存储之后再更新customer_id。所以在多对一,一对多形成双向关联的时候,应该把控制权交给多的一方,这样比较有效率。理由很简单,就像在公司里一样,老板记住所有员工的名字来得快,还是每个员工记住老板的名字来得快。
2.在建立两个对象的关联时,应该同时修改关联两端的相应属性:
Customer.getOrders().add(order);
Order.setCustomer(customer);
这样才会使程序更加健壮,提高业务逻辑层的独立性,使业务逻辑层的程序代码不受Hibernate实现类的影响。同理,当删除双向关联的关系时,也应该修改关联两端的对象的相应属性:
Customer.getOrders().remove(order);
Order.setCustomer(null);
一对一关联与多对多关联,在此就不做总结了。把多对一和一对多搞明白了,这些就容易了!
四、操纵持久化对象
在hibernate中有三个缓存:一个是普通的SessionFactory的缓存,用来存储映射文件和SQL查询语句。一个是Session的缓存(一级缓存)用来存放持久化对象。一个是二级缓存(这个还没学)。
操纵持久化对象,玩的就是一级缓存。Session接口提供save、update、delete、load、get方法,这些方法设置或读取的对象被保 存在一级缓存中,这些对象被称为持久化对象。我们前边提到的所有持久化对象,必须是保存在缓存中的对象。
五、Hibernate在实际项目中遇到的问题
1) 今天用hibernate连接数据库的时候出现这样的错误:
%%% Error Creating SessionFactory %%%,org.hibernate.MappingException: Could not determine type for: String, for columns:
hibernate的映射文件配置错误,因为这个映射文件是我手工编写的,所以开始的时候以为是与数据库中的表对应错误,或者表的字段对应错误,检查了一上午,最后仔细检查映射文件,发现string类型中有大写有小写,于是全部改成小写,错误解决了。。。
查阅了一下,“String”是JAVA类型,而"string"才是hibernate类型,人生啊,一个字母忙乎了半天,日了!!!
2) Hibernate BeanCreationException 异常错误(should be mapped with insert="false" update="false")
不同的属性映射为相同的字段,重复映射同一个字段,就会出现这个错误;
3)Error applying BeanValidation relational constraints。
解决办法:由于javax.persistence.validation.mode的属性值默认是auto,所以会出错。在hibernate.cfg.xml里将javax.persistence.validation.mode设置为none,就可以避免出错了。
<property name="javax.persistence.validation.mode">none</property>
单向一对多关联
domain的定义如下:
Account: private Integer id; private String loginname; private String password; private String email; private Date registrationTime; private Set<Order> orders; Order: private Integer id; private String orderNo; private float price;
关键配置文件的定义如下:
Account.hbm.xml <set name="orders" lazy="false" cascade=" "> <key column="cid" not-null="true" /> <one-to-many class="Order"/> </set>
单向多对一关联
domain的定义如下:
Account: private Integer id; private String loginname; private String password; private String email; private Date registrationTime; Order: private Integer id; private String orderNo; private float price;
private Account account;
关键配置文件的定义如下:
Order.hbm.xml
<many-to-one name="account" column="cid" class="Account" cascade=" "/>
在 关系型数据库理论中,“多对一”关联同于“一对多”关联,且为了消除数据冗余,在两个关系之间不存在“多对多”关联,“多对多”关联要通过连接表来实现。 因此在关系型数据库中只有“一对一”和“一对多(多对一)”,且都是单向的。而在hibernate当中,为了保证关联双方的映射可以通过多种方式进行, “单向一对多”关联和“单向多对一”被认为是两种不同的关联,其主要区别是在于哪个表的映射文件中进行<many to one>的配置, 进行<many to one>配置的一方便是“多”。 由于“单向一对多”关联的应用比较少见并不被hibernate推荐使用,下面仅对"单向多对一"关联进行说明 (提倡此种用法,提高hibernate性能) 对于<many to one>中,主要属性说明如下
name(必需): 设定“many”方所包含的“one”方所对应的持久化类的属性名
column(可选): 设定one方的主键,即持久化类的属性对应的表的外键
class(可选): 设定one方对应的持久化类的名称,即持久化类属性的类型
not-null(可选): 如果为true,,表示需要建立相互关联的两个表之间的外键约束
cascade(可选): 级联操作选项,默认为none
建立单向多对一关联需要注意以下几点问题
1 .在many方的映射文件中,使用<many to one>标识进行关联映射的定义
2 .many方的持久化类中必须声明one方对应的持久化类的属性,用于实现多对一的导航,如上例中的private Address address;
3 .应用程序中必须创建 many 方到one方的关联,示例中使用带有one方对应的持久化类的参数类型的构造函数是一种方式,另外也可以直接使用many方的set方法创建关联,如pl.setAddress(addr)
4. 级联操作定义在<many to one>操作中,如果将cascade属性设为all或save-update,那么在应用程序中只需要用 Session的sava方法持久化many方的对象即可,如 session.save(p3)