Hibernate入门19 - 双向关联与inverse设定
之前我们对User与Room作了单向的多对一以及反过来的一对多关联,我们也可以让User与Room彼此参考,形成双向关联,就User与Room对象,具体来说,就是将程序如下设计:
User.java
package onlyfun.caterpillar;
public class User {
private long id;
private String name;
private Room room;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Room getRoom() {
return room;
}
public void setRoom(Room room) {
this.room = room;
}
}
Room.java
package onlyfun.caterpillar;
import java.util.*;
public class Room {
private long id;
private String address;
private Set users = new HashSet();
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Set getUsers() {
return users;
}
public void setUsers(Set users) {
this.users = users;
}
}
而其对应的映射文件如下,首先是User.hbm.xml:
User.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.User" table="USER">
<id name="id" column="USER_ID" unsaved-value="0">
<generator class="increment"/>
</id>
<property name="name">
<column name="NAME" length="16" not-null="true"/>
</property>
<many-to-one name="room"
column="ROOM_ID"
class="onlyfun.caterpillar.Room"/>
</class>
</hibernate-mapping>
再来是Room.hbm.xml:
Room.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.Room" table="ROOM">
<id name="id" column="ROOM_ID" unsaved-value="0">
<generator class="increment"/>
</id>
<property name="address" type="string"/>
<set name="users" table="USER" cascade="all">
<key column="ROOM_ID"/>
<one-to-many class="onlyfun.caterpillar.User"/>
</set>
</class>
</hibernate-mapping>
这就形成了User与Room之间的双向关联映像,我们可以使用以下的程序进行测试:
HibernateTest.java
import onlyfun.caterpillar.*;
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.*;
public class HibernateTest {
public static void main(String[] args) throws HibernateException {
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Room room = new Room();
room.setAddress("NTU-M8-419");
User user1 = new User();
user1.setName("bush");
User user2 = new User();
user2.setName("bush");
/*
* 因为没有设定inverser,所以只须从parent维护即可
*/
//user1.setRoom(room);
//user2.setRoom(room);
room.getUsers().add(user1);
room.getUsers().add(user2);
Session session = sessionFactory.openSession();
Transaction tx= session.beginTransaction();
session.save(room);
tx.commit();
session.close();
sessionFactory.close();
}
}
基本上就数据的储存来说,这样就已经足够,但这样的设计会有效能问题,显然的,这个程序将Room与User之间的关联交由Room来维持,就Room 而言,它要先储存自已,然后储存其所包括的多个User,之后再对每一个User更新(update)对自己(Room)的关联,具体而言,这个程序必须实行以下的SQL:
Hibernate: insert into ROOM (address, ROOM_ID) values (?, ?)
Hibernate: insert into USER (NAME, ROOM_ID, USER_ID) values (?, ?, ?)
Hibernate: insert into USER (NAME, ROOM_ID, USER_ID) values (?, ?, ?)
Hibernate: update USER set ROOM_ID=? where USER_ID=?
Hibernate: update USER set ROOM_ID=? where USER_ID=?
就Room而言,它并不知道其所包括的User是不是一个已储存的对象,或者即使为已储存对象,也不知道USER表格 上的ROOM_ID是不是参考至ROOM表格的ROOM_ID上,所以它必须针对自己所包括的User对象一个个进行更新,以确保USER表格上的 ROOM_ID是指向自己。
如果将关联的维护交给User的话会比较容易,因为每个User都对应至一个Room,在储存时并用像Room一样必须对Set中的每个对象作检查,为 了将关联的维护交给User,我们可以在Room.hbm.xml中的<set>修改,加上inverse="true",表示将关联的维护「反过来」交给User作:
Room.java
<set name="users" table="USER" inverse="true" cascade="all">
<key column="ROOM_ID"/>
<one-to-many class="onlyfun.caterpillar.User"/>
</set>
由于将关联的维护交给User来作了,所以我们必须在储存时,明确的将Room设定给User,也就是说,必须这样作:
/*
* 因为有user维护关联,所以必须呼叫setRoom
*/
user1.setRoom(room);
user2.setRoom(room);
room.getUsers().add(user1);
room.getUsers().add(user2);
这比不加上inverse="true"设定时多了个指定的动作,您必须多键几个字,所带来的是效率上的增加,Hibernate的持久层管理员会先储存Room,然后储存User,如此就可以省去之前再进行更新的动作,具体来说,就是会执行以下的SQL:
Hibernate: insert into ROOM (address, ROOM_ID) values (?, ?)
Hibernate: insert into USER (NAME, ROOM_ID, USER_ID) values (?, ?, ?)
Hibernate: insert into USER (NAME, ROOM_ID, USER_ID) values (?, ?, ?)
与先前不同的是,由于关联交给了User维护,所以这次Room不用一一更新USER以确定每个ROOM_ID都指向自已。
如果指定了inverse="true",而不确实的将Room指定给User会如何?那么User与Room会各自储存,但彼此没有关联,也就是User将不会参考至Room,USER表格的ROOM_ID将为null,例如:
mysql> select * from USER;
+---------+------+---------+
| USER_ID | NAME | ROOM_ID |
+---------+------+---------+
| 1 | bush | NULL |
+---------+------+---------+
mysql> select * from ROOM;
+---------+------------+
| ROOM_ID | address |
+---------+------------+
| 1 | NTU-M8-419 |
+---------+------------+
作个总结,在设立双向关联时,关联由多对一中「多」的哪一方维护,会比由「一」的哪一方维护来的方便,在Hibernate可以藉由inverse来设定,不设定inverse基本上也可以运行,但是效能会较差。
设定了inverse,必须要明确的设定双方的参考,以这个主题的例子,Room要设定给User,而User也要知道Room的存在,这比不设定 inverse需要键入较多的字,但从另一方面,比较符 合程序设计的直觉(单看User与Room类别,两者要互相参考时,本来就要明确设定)。
Hibernate入门20 - 一对一实体映像
假设我们之前范例的User与Room是一对一的关系,也就是每一个人分配一个房间,先看看这两个类别:
User.java
package onlyfun.caterpillar;
public class User {
private long id;
private String name;
private Room room;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Room getRoom() {
return room;
}
public void setRoom(Room room) {
this.room = room;
}
}
Room.java
package onlyfun.caterpillar;
public class Room {
private long id;
private String address;
private User user;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
要映像User与Room的一对一关系,我们可以有两种方式,一种是透过外键参考,在之前的多对一的例子中即使外键参考的例子,我们现在限制多对一为一 对一,只要在User.hbm.xml中的<many-to-one>上加上unique="true",表示限制一个User有一独有的 Room:
User.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.User" table="USER">
<id name="id" column="USER_ID" unsaved-value="0">
<generator class="increment"/>
</id>
<property name="name">
<column name="NAME" length="16" not-null="true"/>
</property>
<many-to-one name="room"
column="ROOM_ID"
class="onlyfun.caterpillar.Room"
cascade="all"
unique="true"/>
</class>
</hibernate-mapping>
这就完成了单向的一对一映射,我们可以在Room.hbm.xml上加入参考回User的设定,使其成为双向的一对一映射,如下:
Room.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.Room" table="ROOM">
<id name="id" column="ROOM_ID" unsaved-value="0">
<generator class="increment"/>
</id>
<property name="address" type="string"/>
<one-to-one name="user"
class="onlyfun.caterpillar.User"
property-ref="room"/>
</class>
</hibernate-mapping>
在<one-to-one>的设定中,我们告诉Hibernate,Room返向参考回User的room属性。
使用以下的程序来测试数据的储存:
HibernateTest.java
import onlyfun.caterpillar.*;
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.*;
public class HibernateTest {
public static void main(String[] args) throws HibernateException {
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Room room = new Room();
room.setAddress("NTU-M8-419");
User user1 = new User();
user1.setName("bush");
user1.setRoom(room);
room.setUser(user1);
Session session = sessionFactory.openSession();
Transaction tx= session.beginTransaction();
session.save(user1);
tx.commit();
session.close();
sessionFactory.close();
}
}
数据表的实际例子,与多对一映像时相同,只不过现在一个User只能对应一个Room。
另一个映像一对一的方式是使用主键关联,限制两个数据表的主键使用相同的值,如此一个User与Room就是一对一关系,在User.hbm.xml这边,只要使用<one-to-one>设定关联即可:
User.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.User" table="USER">
<id name="id" column="USER_ID" unsaved-value="0">
<generator class="increment"/>
</id>
<property name="name">
<column name="NAME" length="16" not-null="true"/>
</property>
<one-to-one name="room"
class="onlyfun.caterpillar.Room"
cascade="all"/>
</class>
在Room.hbm.xml这边,必须限制其主键与User的主键相同,而在属性上,使用constrained="true"告诉Hibernate参考至User的主键:
Room.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.Room" table="ROOM">
<id name="id" column="ROOM_ID" unsaved-value="0">
<generator class="foreign">
<param name="property">user</param>
</generator>
</id>
<property name="address" type="string"/>
<one-to-one name="user"
class="onlyfun.caterpillar.User"
constrained="true"/>
</class>
</hibernate-mapping>
只要改变映像文件即可,程序的部份无需修改,数据库中的实际储存例子如下:
mysql> select * from USER;
+---------+-------------+
| USER_ID | NAME |
+---------+-------------+
| 1 | bush |
| 2 | caterpillar |
+---------+-------------+
2 rows in set (0.00 sec)
mysql> select * from ROOM;
+---------+------------+
| ROOM_ID | address |
+---------+------------+
| 1 | NTU-M8-419 |
| 2 | NTU-M8-420 |
+---------+------------+
2 rows in set (0.00 sec)
Hibernate入门21 - 多对多实体映像
假设现在有User与Server两个类别,一个User可以被授权使用多台Server,而在Server上也记录授权使用它的使用者,就User与Server两者而言即使多对多的关系。
在程序设计时,基本上是不建议直接在User与Server之间建立多对多关系,这会使得User与Server相互依赖,通常会透过一个中介类别来维护两者之间的多对多关系,避免两者的相互依赖。
如果一定要直接建立User与Server之间的多对多关系,Hibernate也是支持的,基本上只要您了解之前介绍的几个实体映像,建立多对多关联在配置上并不困难。
先看一下我们设计的User与Server类别:
User.java
package onlyfun.caterpillar;
import java.util.*;
public class User {
private long id;
private String name;
private Set servers = new HashSet();
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set getServers() {
return servers;
}
public void setServers(Set servers) {
this.servers = servers;
}
}
Server.java
package onlyfun.caterpillar;
import java.util.*;
public class Server {
private long id;
private String address;
private Set users = new HashSet();
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Set getUsers() {
return users;
}
public void setUsers(Set users) {
this.users = users;
}
}
这边各使用HashSet来保存彼此的关系,在多对多关系映射上,我们可以建立单向或双向关系,这边直接介绍双向关系映像,并藉由设定inverse= "true",将关系的维护交由其中一方来维护,这么作的结果,在原始码的撰写上,也比较符合Java的对象关系维护,也就是双方都要设置至对方的参考。
首先来看看User.hbm.xml:
User.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.User" table="USER">
<id name="id" column="USER_ID" unsaved-value="0">
<generator class="increment"/>
</id>
<property name="name">
<column name="NAME" length="16" not-null="true"/>
</property>
<set name="servers"
table="USER_SERVER"
cascade="save-update">
<key column="USER_ID"/>
<many-to-many class="onlyfun.caterpillar.Server"
column="SERVER_ID"/>
</set>
</class>
</hibernate-mapping>
在数据库中,数据表之间的多对多关系是透过一个中介的数据表来完成,例如在这个例子中,USER数据表与USER_SERVER数据表是一对多,而 USER_SERVER对SERVER是多对一,从而完成USER至SERVER的多对多关系,在USER_SERVER数据表中,将会有USER_ID 与SERVER_ID共同作为主键,USER_ID作为一个至USER的外键参考,而SERVER_ID作为一个至SERVER的外键参考。
来看看Server.hbm.xml映射文件:
Server.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.Server" table="SERVER">
<id name="id" column="SERVER_ID" unsaved-value="0">
<generator class="increment"/>
</id>
<property name="address" type="string"/>
<set name="users"
table="USER_SERVER"
inverse="true"
cascade="save-update">
<key column="SERVER_ID"/>
<many-to-many class="onlyfun.caterpillar.User"
column="USER_ID"/>
</set>
</class>
</hibernate-mapping>
设置上与User.hbm.xml是类似的,只是增加了inverse="true",表示将关系的维护交由另一端,注意我们在User与 Server的cascade都是设置为save-update,在多对多的关系中,all、delete等cascade是没有意义的,因为多对多中, 并不能因为父对象被删除,而造成被包括的子对象被删除,因为可能还有其它的父对象参考至这个子对象。
我们使用下面这个程序来测试:
HibernateTest.java
import onlyfun.caterpillar.*;
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.*;
public class HibernateTest {
public static void main(String[] args) throws HibernateException {
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Server server1 = new Server();
server1.setAddress("PC-219");
Server server2 = new Server();
server2.setAddress("PC-220");
Server server3 = new Server();
server3.setAddress("PC-221");
User user1 = new User();
user1.setName("caterpillar");
User user2 = new User();
user2.setName("momor");
user1.getServers().add(server1);
user1.getServers().add(server2);
user1.getServers().add(server3);
server1.getUsers().add(user1);
server2.getUsers().add(user1);
server3.getUsers().add(user1);
user2.getServers().add(server1);
user2.getServers().add(server3);
server1.getUsers().add(user2);
server3.getUsers().add(user2);
Session session = sessionFactory.openSession();
Transaction tx= session.beginTransaction();
session.save(user1);
session.save(user2);
tx.commit();
session.close();
sessionFactory.close();
}
}
注意由于设定了inverse="true",所以必须分别设定User与Server之间的相互参考,来看看实际上数据库中是如何储存的:
mysql> select * FROM USER;
+---------+-------------+
| USER_ID | NAME |
+---------+-------------+
| 1 | caterpillar |
| 2 | momor |
+---------+-------------+
2 rows in set (0.00 sec)
mysql> select * FROM USER_SERVER;
+-----------+---------+
| SERVER_ID | USER_ID |
+-----------+---------+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 1 | 2 |
| 2 | 2 |
+-----------+---------+
5 rows in set (0.00 sec)
mysql> select * FROM SERVER;
+-----------+---------+
| SERVER_ID | address |
+-----------+---------+
| 1 | PC-219 |
| 2 | PC-221 |
| 3 | PC-220 |
+-----------+---------+
3 rows in set (0.00 sec)
有关于多对多更多的例子与说明,您可以参考Hibernate in Action的6.3。
Hibernate入门22 - 延迟初始 Lazy Initialization
首先我们来看看这个主题:
入门 11 - Set 映射
依这个主题所完成的例子,请将Hibernate的show_sql设定为true,当我们使用下面的程序时,观看控制台所使用的SQL:
HibernateTest.java
import onlyfun.caterpillar.*;
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.*;
import java.util.*;
public class HibernateTest {
public static void main(String[] args) throws HibernateException {
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
List users = session.find("from User");
session.close();
sessionFactory.close();
}
}
SQL运作的例子如下:
Hibernate: select user0_.USER_ID as USER_ID, user0_.NAME as NAME from USER user0_
Hibernate: select addrs0_.USER_ID as USER_ID__, addrs0_.ADDRESS as ADDRESS__ from ADDRS addrs0_ where addrs0_.USER_ID=?
Hibernate: select addrs0_.USER_ID as USER_ID__, addrs0_.ADDRESS as ADDRESS__ from ADDRS addrs0_ where addrs0_.USER_ID=?
可以看到的,除了从USER表格中读取数据之外,还向ADDRS表格读取数据,预设上,Hibernate会将所有关联到的对象,透过一连串的SQL语 句读取并加载数据,然而现在考虑一种情况,我们只是要取得所有USER的名称,而不用取得它们的邮件地址,此时自动读取相关联的对象就是不必要的。
在Hibernate中,集合类的映像可以延迟初始(Lazy Initialization),也就是在真正索取该对象的数据时,才向数据库查询,就这个例子来说,就是我们在读取User时,先不取得其中的 addrs属性中之对象数据,由于只需要读取User的name属性,此时我们只要执行一次select即可,真正需要addrs的数据时,才向数据库要 求。
要使用Hibernate的延迟初始功能,只要在集合类映像时,加上lazy="true"即可,例如在我们的User.hbm.xml中的<set>中如下设定:
User.hbm.xml
<set name="addrs" table="ADDRS" lazy="true">
<key column="USER_ID"/>
<element type="string" column="ADDRESS" not-null="true"/>
</set>
我们来看看下面这个程序:
HibernateTest.java
import onlyfun.caterpillar.*;
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.*;
import java.util.*;
public class HibernateTest {
public static void main(String[] args) throws HibernateException {
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
List users = session.find("from User");
for (ListIterator iterator = users.listIterator(); iterator.hasNext(); ) {
User user = (User) iterator.next();
System.out.println(user.getName());
Object[] addrs = user.getAddrs().toArray();
for(int i = 0; i < addrs.length; i++) {
System.out.println("\taddress " + (i+1) + ": " + addrs[i]);
}
}
session.close();
sessionFactory.close();
}
}
在没有使用延迟初始时,控制台会显示以下的内容:
Hibernate: select user0_.USER_ID as USER_ID, user0_.NAME as NAME from USER user0_
Hibernate: select addrs0_.USER_ID as USER_ID__, addrs0_.ADDRESS as ADDRESS__ from ADDRS addrs0_ where addrs0_.USER_ID=?
Hibernate: select addrs0_.USER_ID as USER_ID__, addrs0_.ADDRESS as ADDRESS__ from ADDRS addrs0_ where addrs0_.USER_ID=?
caterpillar
address 1: caterpillar@caterpillar.onlyfun.net
address 2: justin@caterpillar.onlyfun.net
address 3: justin@fake.com
momor
address 1: momor@fake.com
address 2: momor@caterpillar.onlyfun.net
如果使用延迟初始,则会出现以下的内容:
Hibernate: select user0_.USER_ID as USER_ID, user0_.NAME as NAME from USER user0_
caterpillar
Hibernate: select addrs0_.USER_ID as USER_ID__, addrs0_.ADDRESS as ADDRESS__ from ADDRS addrs0_ where addrs0_.USER_ID=?
address 1: caterpillar@caterpillar.onlyfun.net
address 2: justin@caterpillar.onlyfun.net
address 3: justin@fake.com
momor
Hibernate: select addrs0_.USER_ID as USER_ID__, addrs0_.ADDRESS as ADDRESS__ from ADDRS addrs0_ where addrs0_.USER_ID=?
address 1: momor@fake.com
address 2: momor@caterpillar.onlyfun.net
请注意SQL语句出现的位置,在使用延迟初始功能前,会将所有相关联到的数据一次查完,而使用了延迟初始之后,只有在真正需要addrs的数据时,才会使用SQL查询相关数据。
Hibernate实现延迟初始功能的方法,是藉由实现一个代理对象(以Set来说,其实现的代理子类是 net.sf.hibernate.collection.Set),这个代理类实现了其所代理之对象之相关方法,每个方法的实现实际上是委托(delegate)真正的对象,查询时加载的是代理对象,在真正呼叫对象的相关方法之前,不会去初始真正的对象来执行被呼叫的方法。
所以为了能使用延迟初始,您在使用集合映像时,在宣告时必须是集合类的接口,而不是具体的实现类(例如宣告时使用Set,而不是HashSet)。
使用延迟初始的一个问题是,由于在需要时才会去查询数据库,所以session不能关闭,如果在session关闭后,再去要求被关联的对象,将会发生LazyInitializationException,像是:
Set addrs = user.getAddrs();
session.close();
// 下面这句会发生LazyInitializationException
Object[] addrs = user.getAddrs().toArray();
如果您使用了延迟初始,而在某些时候仍有需要在session关闭之后取得相关对象,则可以使用Hibernate.initialize()来先行加载相关对象,例如:
Hibernate.initialize(user.getAddrs());
session.close();
Set add = user.getAddrs();
Object[] addo = user.getAddrs().toArray();
延迟初始只是Hibernate在取得数据时的一种策略,目的是为了调节数据库存取时的时机以取得一些效能,除了延迟初始之外,还有其它的策略来调整资料库存取的方法与时机,这部份牵涉的讨论范围很大,有兴趣的话,可以参考Hibernate in Action的
Hibernate入门23 - Session 管理
Session是Hibernate运作的中心,对象的生命周期、事务的管理、数据库的存取,都与 Session息息相关,就如同在编写JDBC时需关心Connection的管理,以有效的方法创建、利用与回收Connection,以减少资源的消耗,增加系统执行效能一样,有效的Session管理,也是Hibernate应用时需关注的焦点。
Session是由SessionFactory所创建,SessionFactory是执行绪安全的(Thread-Safe),您可以让多个执行绪 同时存取SessionFactory而不会有数据共享的问题,然而Session则不是设计为执行绪安全的,所以试图让多个执行绪共享一个 Session,将会发生数据共享而发生混乱的问题。
在Hibernate参考手册中的第一章快速入门中,示范了一个HibernateUtil,它使用了ThreadLocal类别来建立一个 Session管理的辅助类,这是Hibernate的Session管理一个广为应用的解决方案,ThreadLocal是Thread- Specific Storage模式的一个运作实例,您可以在下面这篇文章中了解Thread-Specific Storage模式:
designpattern:Thread-Specific Storage
由于Thread-Specific Stroage模式可以有效隔离执行绪所使用的数据,所以避开Session的多执行绪之间的数据共享问题,以下列出Hibernate参考手册中的HibernateUtil类:
HibernateUtil.java
import net.sf.hibernate.*;
import net.sf.hibernate.cfg.*;
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory;
static {
try {
// Create the SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("Initial SessionFactory creation failed.", ex);
throw new ExceptionInInitializerError(ex);
}
}
public static final ThreadLocal session = new ThreadLocal();
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// Open a new Session, if this Thread has none yet
if (s == null) {
s = sessionFactory.openSession();
session.set(s);
}
return s;
}
public static void closeSession() throws HibernateException {
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}
在同一个执行绪中,Session被暂存下来了,但无须担心数据库连结Connection持续占用问题,Hibernate会在真正需要数据库操作时才(从连接池中)取得Connection。
在Web应用程序中,我们可以藉助Filter来进行Session管理,在需要的时候开启Session,并在Request结束之后关闭Session,这个部份,在JavaWorld的Wiki上有个很好的例子:
http://www.javaworld.com.tw/confluence/pages/viewpage.action?pageId=805
另外在Hibernate中文网上也有一篇介绍:
http://www.hibernate.org.cn/80.html
Hibernate入门24 - Criteria 查询
Hibernate支持一种符合Java撰写习惯的查询API,使用Session建立一个net.sf.hibernate.Criteria,您可以在不使用SQL甚至HQL的情况下进行对数据库的查询。
我们以之前所练习过的第一个Hibernate程序完成的结果为例:
入门 02 - 第一个Hibernate程序
如果要使用Criteria来查询所有的User数据,则如下撰写:
Criteria crit = session.createCriteria(User.class);
List users = crit.list();
for (ListIterator iterator = users.listIterator(); iterator.hasNext(); ) {
User user = (User) iterator.next();
System.out.println("name: " + user.getName());
System.out.println("age: " + user.getAge());
}
如果我们要为查询限定条件,则可以透过net.sf.hibernate.expression.Expression设定查询条件,Expression拥有许多条件查询方法,举个实际的例子说明:
Criteria crit = session.createCriteria(User.class);
crit.add(Expression.ge("age", new Integer(25)));
List users = crit.list();
Expression的ge()方法即great-equal,也就是大于等于(>=),在上例中我们设定查询age属性大于等于25的User数据。
您也可以设定多个查询条件,例如:
crit.add(Expression.gt("age", new Integer(20)));
crit.add(Expression.between("weight", new Integer(60),new Integer(80)));
List users = crit.list();
上例中我们查询age大于20,而weight介于60到80之间的User。
您也可以使用逻辑组合来进行查询,例如:
crit.add(Expression.or(
Expression.eq("age", new Integer(20)),
Expression.isNull("age")
));
List users = crit.list();
如果要对结果进行排序,可以使用net.sf.hibernate.expression.Order,例如:
List cats = session.createCriteria(User.class)
.add(Expression.ge("age", new Integer(20));
.addOrder( Order.asc("name") )
.addOrder( Order.desc("age") )
.setMaxResults(50)
.list();
setMaxResults()方法可以限定查询回来的笔数,如果配合setFirstResult()设定传回查询结果第一笔数据的位置,就可以实现简单的分页,例如:
Criteria crit = session.createCriteria(User.class);
crit.setFirstResult(51);
crit.setMaxResult(50);
List users = crit.list();
上面的例子将传回第51笔之后的资料(51到100)。
实际上,Criteria、Expression、Order等是对SQL进行了封装,让Java程序设计人员可以用自已习惯的语法来撰写查询,而不用使用HQL或SQL(有兴趣的话,可以在组态档中设定显示SQL,看看对应的SQL),然而Criteria在Hibernate中功能还不是很完美,只 能实现一些较为简单的查询,对于查询,Hibernate仍鼓励使用HQL作为查询的首选方式。
Criteria的使用相当简单,这边只介绍一些基本,有兴趣的话,在Hibernate参考手册的第12章中介绍有更多的查询方式。
Hibernate入门25 - 事务管理
事务是一组原子操作(一组SQL执行)的工作单元,这个工作单元中的所有原子操作在进行期间,与其它事务隔离,免于数据源的交相更新而发生混乱,事务中的所有原子操作,要嘛全部执行成功,要嘛全部失败(即使只有一个失败,所有的原子操作也要全部撤消)。
在JDBC中,可以用Connection来管理事务,可以将Connection的AutoCommit设定为false,在下达一连串的SQL语句后,自行呼叫Connection的commit()来送出变更,如果中间发生错误,则撤消所有的执行,例如:
try {
.....
connection.setAutoCommit(false);
.....
// 一连串SQL操作
connection.commit();
} catch(Exception) {
// 发生错误,撤消所有变更
connection.rollback();
}
Hibernate本身没有事务管理功能,它依赖于JDBC或JTA的事务管理功能,预设是使用JDBC事务管理,事实上,下面的程序只是对JDBC事务管理作了个简单的封装:
try {
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
....
tx.commit();
} catch(Exception e) {
tx.rollback();
}
在执行openSession()时,实际上Hibernate是在开启一个Connection,而执行beginTransaction()时,实 际上会执行Connection的setAutoCommit(false)方法,最后的tx.commit()就是呼叫Connection的 commit()方法,有兴趣的话,可以研究net.sf.hibernate.transaction.JDBCTransaction中的begin ()与commit()方法,可以找到对应的JDBC代码。
session = sessionFactory.openSession(); <-- Connection connection = ....;
tx = session.beginTransactioin(); <-- connection.setAutoCommit(false);
tx.commit(); <-- connection.commit();
session.close(); <-- connection.close();
所以使用JDBC事务管理,最后一定要执行Transaction的commit(),如果没有,则之前对session所下的所有指令并不会有效果。
Hibernate可以透过配置文件来使用基于JTA的事务管理,JTA事务管理可以跨越数个Session,而不是像JDBC事务管理只能在一个Session中进行,我们在hibernate.cfg.xml中如下撰写:
<hibernate-configuration>
<session-factory>
....
<property name="hibernate.transaction.factory_class">
net.sf.hibernate.transaction.JTATransactionFactory
</property>
....
</session-factory>
</hibernate-configuration>
或者是在hibernate.properties中撰写:
hibernate.transaction.factory_class=net.sf.hibernate.transaction.JTATransactionFactory
JTA的事务是由JTA容器管理,而不是像JDBC事务管理一样由Connection来管理事务,所以我们不是由Session来开始事务,而是如下进行:
javax.transaction.UserTransaction tx = new InitialContext().lookup("javax.transaction.UserTransaction");
Session s1 = sessionFactory.openSession();
.... // 一些save、update等
s1.flush();
s1.close();
...
Session s2 = sf.openSession();
...
s2.flush();
s2.close();
....
tx.commit();
同样的,如果您有兴趣,可以看net.sf.hibernate.transaction.JTATransaction中的begin()与commit()方法代码,看看Hibernate如何封装JTA的事务管理代码的。
Hibernate入门26 - 悲观锁定
在多个客户端可能读取同一笔数据或同时更新一笔数据的情况下,必须要有访问控制的手段,防止同一个数据被修改而造成混乱,最简单的手段就是对数据进行锁定,在自己进行数据读取或更新等动作时,锁定其它客户端不能对同一笔数据进行任何的动作。
悲观锁定(Pessimistic Locking)一如其名称所示,悲观的认定每次资料存取时,其它的客户端也会存取同一笔数据,因此对该笔数据进行锁定,直到自己操作完成后解除锁定。
悲观锁定通常透过系统或数据库本身的功能来实现,依赖系统或数据库本身提供的锁定机制,Hibernate即是如此,我们可以利用Query或Criteria的setLockMode()方法来设定要锁定的表或列(row)及其锁定模式,锁定模式有以下的几个:
- LockMode.WRITE:在insert或update时进行锁定,Hibernate会在save()方法时自动获得锁定。
- LockMode.UPGRADE:利用SELECT … FOR UPDATE进行锁定。
- LockMode.UPGRADE_NOWAIT:利用SELECT … FOR UPDATE NOWAIT进行锁定,在Oracle环境下使用。
- LockMode.READ:在读取记录时Hibernate会自动获得锁定。
- LockMode.NONE:没有锁定。
也可以在使用Session的load()或是lock()时指定锁定模式以进行锁定。
如果数据库不支持所指定的锁定模式,Hibernate会选择一个合适的锁定替换,而不是丢出一个例外(Hibernate参考手册10.6)。
Hibernate入门 27 - 乐观锁定
悲观锁定假定任何时刻存取数据时,都可能有另一个客户也正在存取同一笔数据,因而对数据采取了数据库层次的锁定状态,在锁定的时间内其它的客户不能对资 料进行存取,对于单机或小系统而言,这并不成问题,然而如果是在网络上的系统,同时间会有许多联机,如果每一次读取数据都造成锁定,其后继的存取就必须等待,这将造成效能上的问题,造成后继使用者的长时间等待。
乐观锁定(optimistic locking)则乐观的认为资料的存取很少发生同时存取的问题,因而不作数据库层次上的锁定,为了维护正确的数据,乐观锁定使用应用程序上的逻辑实现版本控制的解决。
例如若有两个客户端,A客户先读取了账户余额1000元,之后B客户也读取了账户余额1000元的数据,A客户提取了500元,对数据库作了变更,此时 数据库中的余额为500元,B客户也要提取300元,根据其所取得的资料,1000-300将为700余额,若此时再对数据库进行变更,最后的余额就会不正确。
在不实行悲观锁定策略的情况下,数据不一致的情况一但发生,有几个解决的方法,一种是先更新为主,一种是后更新的为主,比较复杂的就是检查发生变动的数据来实现,或是检查所有属性来实现乐观锁定。
Hibernate中透过版本号检查来实现后更新为主,这也是Hibernate所推荐的方式,在数据库中加入一个VERSON栏记录,在读取数据时连 同版本号一同读取,并在更新数据时递增版本号,然后比对版本号与数据库中的版本号,如果大于数据库中的版本号则予以更新,否则就回报错误。
以刚才的例子,A客户读取账户余额1000元,并连带读取版本号为5的话,B客户此时也读取账号余额1000元,版本号也为5,A客户在领款后账户余额 为500,此时将版本号加1,版本号目前为6,而数据库中版本号为5,所以予以更新,更新数据库后,数据库此时余额为500,版本号为6,B客户领款后要变更数据库,其版本号为5,但是数据库的版本号为6,此时不予更新,B客户数据重新读取数据库中新的数据并重新进行业务流程才变更数据库。
以Hibernate实现版本号控制锁定的话,我们的对象中增加一个version属性,例如:
public class Account {
private int version;
....
public void setVersion(int version) {
this.version = version;
}
public int getVersion() {
return version;
}
....
}
而在映像文件中,我们使用optimistic-lock属性设定version控制,<id>属性栏之后增加一个<version>标签,例如:
<hibernate-mapping>
<class name="onlyfun.caterpillar.Account" talble="ACCOUNT"
optimistic-lock="version">
<id...../>
<version name="version" column="VERSION"/>
....
</class>
</hibernate-mapping>
设定好版本控制之后,在上例中如果B客户试图更新数据,将会引发StableObjectStateException例外,我们可以捕捉这个例外,在处理中重新读取数据库中的数据,同时将B客户目前的数据与数据库中的数据秀出来,让B客户有机会比对不一致的数据,以决定要变更的部份,或者您可以设计程 式自动读取新的资料,并重复扣款业务流程,直到数据可以更新为止,这一切可以在背景执行,而不用让您的客户知道。
浙公网安备 33010602011771号