Hibernate

Hibernate3
基本配置
配置文件
Hibernate可以使用XML档案或properties档案来配置SessionFactory,预设的配置文件名称为 hibernate.cfg.xml
或hibernate.properties,XML提供较好的结构与配置方式,Hibernate建议使用XML档案进行配置。
前几个主题中所示范的为使用XML文件的方式,一个XML文件的例子如下:
hibernate.cfg.xml

"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">




true



org.hibernate.dialect.MySQLDialect



com.mysql.jdbc.Driver



jdbc:mysql://localhost/demo



caterpillar



123456





使用XML文件进行配置时,可以在当中指定对象与数据库表格的映像文件位置,XML配置文件的位置必须在Classpath下,
使用下面的方式来读入XML文件以配置Hibernate:
Configuration config = new Configuration().configure();
Configuration的实例管理Hibernate的配置讯息,通常用于建立SessionFactory,例如:
SessionFactory sessionFactory = config.buildSessionFactory();
SessionFactory一旦建立,就被赋予当时Configuration的配置讯息,之后您改变Configuration并不会影响已建立的 SessionFactory实例,如果对Configuration改动后,则要建立一个新的SessionFactory实例,新的实例中会包括新的配置讯息。
SessionFactory中包括了数据库配置及映像关系,它的建立相当复杂,所以使用时需考虑到重用已建立的SessionFactory实例,SessionFactory是被设计为「执行绪安全的」(Thread-safe)。
预设的XML配置文件名称是hibernate.cfg.xml,您也可以自行指定档案,例如:
Configuration config = new Configuration().configure().configure("db.cfg.xml");
SessionFactory sf = config.buildSessionFactory();
除了使用XML文件进行配置,您也可以使用属性档案进行配置,文件名称是hibernate.properties,一个例子如下:
hibernate.properties
hibernate.show_sql = true
hibernate.dialect = org.hibernate.dialect.MySQLDialect
hibernate.connection.driver_class = com.mysql.jdbc.Driver
hibernate.connection.url = jdbc:mysql://localhost/demo
hibernate.connection.username = caterpillar
hibernate.connection.password = 123456
hibernate.properties的位置必须在Classpath下,由于properties档案中不包括映像文件的名称,为了要取得
对象至数据库表格的映像文件,您必须在程序中如下加载:
Configuration cfg = new Configuration().addClass("User.hbm.xml").addClass(onlyfun.caterpillar.Item.class);
第一个addClass()加入位于Classpath根目录下的User.hbm.xml,第二个addClass()加入Item类别的映射文件,该文件必
须位于与User类别同一个目录,也就是onlyfun/caterpillar/Item.hbm.xml。
在Hibernate下载档案中的etc目录下,有hibernate.cfg.xml与hibernate.properties可供设定参考。
数据库连结
Hibernate的配置文件中有一部份是在设定数据库连结,例如使用XML档案进行配置:
hibernate.cfg.xml

"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">




true



org.hibernate.dialect.MySQLDialect



com.mysql.jdbc.Driver



jdbc:mysql://localhost/demo



caterpillar



123456



2





其中设定的connection.pool_size是Hibernate预设的连接池设定,通常只用于开发阶段测试之用。如果使用properties档案的话则如下:
hibernate.properties
hibernate.show_sql = true
hibernate.dialect = org.hibernate.dialect.MySQLDialect
hibernate.connection.driver_class = com.mysql.jdbc.Driver
hibernate.connection.url = jdbc:mysql://localhost/demo
hibernate.connection.username = caterpillar
hibernate.connection.password = 123456
hibernate.connection.pool_size = 2
Hibernate在数据库连接池的使用上是可选的,您可以使用C3P0连接池,例如XML的配置方式如下:
hibernate.cfg.xml

"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">




true



org.hibernate.dialect.MySQLDialect



com.mysql.jdbc.Driver



jdbc:mysql://localhost/demo



caterpillar



123456



5


20


1800


50





记得您的Classpath中必须包括c3p0-*.jar,属性文件hibernate.properties的配置范例如下:
hibernate.properties
hibernate.show_sql = true
hibernate.dialect = org.hibernate.dialect.MySQLDialect
hibernate.connection.driver_class = com.mysql.jdbc.Driver
hibernate.connection.url = jdbc:mysql://localhost/demo
hibernate.connection.username = caterpillar
hibernate.connection.password = 123456
hibernate.c3p0.min_size=5
hibernate.c3p0.max_size=20
hibernate.c3p0.timeout=1800
hibernate.c3p0.max_statements=50
您也可以使用Proxool或DBCP连接池,只要在配置文件中配置hibernate.proxool.*或hibernate.dbcp.*等相关选项,这可以在hibernate的etc目录中找hibernate.properties中的配置例子来参考,当然要记得在Classpath中加入相关的jar档案。
如果您使用Tomcat的话,您也可以透过它提供的DBCP连接池来取得连接,您可以先参考 使用 DBCP 的文章来设定Tomcat的DBCP连接池。
设定好容器提供的DBCP连接池之后,您只要在配置文件中加入connection.datasource属性,例如在hibernate.cfg.xml中加入:
java:comp/env/jdbc/dbname
如果是在hibernate.properties中的话,则加入:
hibernate.connection.datasource = java:comp/env/jdbc/dbname
简介快取(Session Level)
数据库每一次的查询都是一次不小的开销,例如连结的开启、执行查询指令,当数据库与应用服务器不在同一个服务器上时,还必须有远程调用、Socket的建立等开销,在Hibernate这样的ORM框架中,还有数据的封装等开销必须考虑进去。
快取(Cache)是数据库在内存中的临时容器,从数据库中读取的数据在快取中会有一份临时拷贝,当您查询某个数据时,会先在快取中寻找是否有相对应的拷贝,如果有的话就直接返回数据,而无需连接数据库进行查询,只有在快取中找不到数据时,才从数据库中查询数据,藉由快取,可以提升应用程序读取数据时的效能。
对于Hibernate这样的ORM框架来说,快取的机制更形重要,在Hibernate中快取分作两个层级:Session level与SessionFactory level(又称Second level快取)。
这边先介绍Session level的快取,在Hibernate中Session level快取会在使用主键加载数据或是延迟初始(Lazy Initialization) 时作用,Session level的快取随着Session建立时建立,而Session销毁时销毁。
Session会维护一个Map容器,并保留与目前Session发生关系的资料,当您透过主键来加载数据时,Session会先依据所要加载的类别与所给定的主键,看看Map中是否已有数据,如果有的话就返回,若没有就对数据库进行查询,并在加载数据后在Map中维护。
可以透过==来比较两个名称是否参考至同一个对象,以检验这个事实:
Session session = sessionFactory.openSession();
User user1 = (User) session.load(User.class, new Integer(1));
User user2 = (User) session.load(User.class, new Integer(1));
System.out.println(user1 == user2);
session.close();
第二次查询数据时,由于在快取中找到数据对象,于是直接返回,这与第一次查询到的数据对象是同一个实例,所以会显示true的结果。
可以透过evict()将某个对象从快取中移去,例如:
Session session = sessionFactory.openSession();
User user1 = (User) session.load(User.class, new Integer(1));
session.evict(user1);
User user2 = (User) session.load(User.class, new Integer(1));
System.out.println(user1 == user2);
session.close();
由于user1所参考的对象被从快取中移去了,在下一次查询时,Session在Map容器中找不到对应的数据,于是重新查询数据库并再封装一个对象,所以user1与user2参考的是不同的对象,结果会显示false。
也可以使用clear()清除快取中的所有对象,例如:
Session session = sessionFactory.openSession();
User user1 = (User) session.load(User.class, new Integer(1));
session.clear();
User user2 = (User) session.load(User.class, new Integer(1));
System.out.println(user1 == user2);
session.close();
同样的道理,这次也会显示false。
Session level的快取随着Session建立与销毁,看看下面这个程序片段:
Session session1 = sessionFactory.openSession();
User user1 = (User) session1.load(User.class, new new Integer(1));
session1.close();

Session session2 = sessionFactory.openSession();
User user2 = (User)session2.load(User.class, new Integer(1));
session2.close();

System.out.println(user1 == user2);
第一个Session在关闭后,快取也关闭了,在第二个Session的查询中并无法用到第一个Session的快取,两个Session阶段所查询到的并不是同一个对象,结果会显示false。
在加载大量数据时,Session level 快取的内容会太多,记得要自行执行clear()清除快取或是用evict()移去不使用对象,以释放快取所占据的资源。
Session在使用save()储存对象时,会将要储存的对象纳入Session level快取管理,在进行大量数据储存时,快取中的实例大量增加,最后会导致OutOfMemoryError,可以每隔一段时间使用Session的 flush()强制储存对象,并使用clear()清除快取,例如:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

while(....) { // 大量加载对象时的循环示意
....
session.save(someObject);
if(count % 100 == 0) { // 每100笔资料
session.flush(); // 送入数据库
session.clear(); // 清除快取
}
}

tx.commit();
session.close();
在SQL Server、Oracle等数据库中,可以在Hibernate设定文件中设定属性hibernate.jdbc.batch_size来控制每多少笔数据就送至数据库,例如:
....


....
100
....


在MySQL中则不支持这个功能。
简介事务管理(基于 JDBC )
事务是一组原子(Atomic)操作(一组SQL执行)的工作单元,这个工作单元中的所有原子操作在进行期间,与其它事务隔离,免于数据来源的交相更新而发生混乱,事务中的所有原子操作,不是全部执行成功,就是全部失败(即使只有一个失败,所有的原子操作也要全部撤消)。
在JDBC中,可以用Connection来管理事务,可以将Connection的AutoCommit设定为false,在下达一连串的SQL语句后,自行呼叫Connection的commit()来送出变更,如果中间发生错误,则撤消所有的执行,例如:
try {
.....
connection.setAutoCommit(false);
.....

// 一连串SQL操作

connection.commit();
} catch(Exception) {
// 发生错误,撤消所有变更
connection.rollback();
}
Hibernate本身没有事务管理功能,它依赖于JDBC或JTA的事务管理功能,预设是使用JDBC事务管理,可以在配置文件中加上hibernate.transaction.factory_class属性来指定Transaction的工厂类别,例如:
hibernate.cfg.xml

"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">



org.hibernate.transaction.JDBCTransactionFactory





基于JDBC的事务管理是最简单的方式,事实上,Hibernate基于JDBC的事务管理只是对JDBC作了个简单的封装:
try {
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
....
tx.commit(); // 必须commit才会更新数据库
} catch(HibernateException e) {
tx.rollback();
}
在一开始的openSession()取得Session时,JDBC的Connection实例之AutoCommit就被设定为false,在 beginTransaction()时,会再度检查Connection实例的AutoCommit为false,在操作过程中,最后要commit (),否则的话对数据库的操作不会有作用,如果操作过程中因发生例外,则最后commit()不会被执行,之前的操作取消,执行rollback()可撤消之前的操作,一个实际的程序如下所示
FirstHibernate2.java
package onlyfun.caterpillar;

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

import org.hibernate.cfg.Configuration;

public class FirstHibernate2 {
public static void main(String[] args) {
Configuration config = new Configuration().configure();
SessionFactory sessionFactory = config.buildSessionFactory();

User user = new User();
user.setName("momor");
user.setAge(new Integer(26));

Session session = null;
Transaction tx = null;

try {
session = sessionFactory.openSession();
tx = session.beginTransaction();
session.save(user);
tx.commit();
} catch (Exception e) {
e.printStackTrace();

if (tx != null) {
try {
tx.rollback();
} catch (HibernateException ee) {
ee.printStackTrace();
}
}
} finally {
if (session != null) {
try {
session.close();
} catch (HibernateException e) {
e.printStackTrace();
}
}
}

sessionFactory.close();
}
}
PS. 要使用MySQL中的事务处理,必须建立事务表类型的表格,例如InnoDB的表格:
CREATE TABLE user (
.....
....
) TYPE = InnoDB;

映射文件
Hibernate 中将对象与数据库表格映像关系连接起来的是映射文件,通常以*.hbm.xml作为文件名称,映射文件可以手工撰写,或是透过工具程序从数据库表格自动生成,或是透过工具程序从Java类别自动生成。
来看看一个基本的映射文件如何撰写:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">












映射文件中主要包括三个部份:类别名称与表格名称的映像、id属性与主键的映像、类别属性与表格字段的映像。
这份映像文件对应于以下的类别与表格:
User.java
package onlyfun.caterpillar;

public class User {
private Integer id;
private String name;
private Integer age;

// 必须要有一个预设的建构方法
// 以使得Hibernate可以使用Constructor.newInstance()建立对象
public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}
user 表格
+-------+------------------+-------+------+----------+---------------------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+-------+------+----------+---------------------+
| id | int(11) | | PRI | NULL | auto_increment |
| name | varchar(100) | | | | |
| age | int(11) | YES | | NULL | |
+-------+------------------+-------+------+----------+---------------------+
的设定上,name设定类别上的属性名,而column对应至表格字段,在type上可以设定Java类别的数据型态,但由于 Java的数据型态与数据库的数据型态并不是一对一对应的,为此Hibernate提供它自己的数据型态,作为Java数据型态与数据库数据型态的连接型态,下面的表格列出型态之间的对应:
Java数据型态 Hibernate数据型态 标准SQL数据型态
byte、java.lang.Byte byte TINYINT
short、java.lang.Short short SMALLINT
int、java.lang.Integer integer INGEGER
long、java.lang.Long long BIGINT
float、java.lang.Float float FLOAT
double、java.lang.Double double DOUBLE
java.math.BigDecimal big_decimal NUMERIC
char、java.lang.Character character CHAR(1)
boolean、java.lang.Boolean boolean BIT
java.lang.String string VARCHAR
boolean、java.lang.Boolean yes_no CHAR(1)('Y'或'N')
boolean、java.lang.Boolean true_false CHAR(1)('Y'或'N')
java.util.Date、java.sql.Date date DATE
java.util.Date、java.sql.Time time TIME
java.util.Date、java.sql.Timestamp timestamp TIMESTAMP
java.util.Calendar calendar TIMESTAMP
java.util.Calendar calendar_date DATE
byte[] binary VARBINARY、BLOB
java.lang.String text CLOB
java.io.Serializable serializable VARBINARY、BLOB
java.sql.Clob clob CLOB
java.sql.Blob blob BLOB
java.lang.Class class VARCHAR
java.util.Locale locale VARCHAR
java.util.TimeZone timezone VARCHAR
java.util.Currency currency VARCHAR
设定主键的生成方式,可以设定"native"表示由Hibernate自动根据Dialect选择采用 identity、hilo、sequence等作为主键生成方式,也可以考虑采用uuid由Hibernate根据128位UUID算法(128- bit UUID algorithm)生成16进位制数值,并编码为32位长度的字符串,还有其它的主键生成方式,可以参考官方手册的 Generator 说明。
基本 API
Session(新)
Hibernate在对数据库进行操作之前,必须先取得Session实例,相当于JDBC在对数据库操作之前,必须先取得Connection实例, Session是Hibernate操作的基础,它不是设计为执行绪安全(Thread-safe),一个Session由一个执行绪来使用。
Session实例由SessionFactory开启获得,例如:
Configuration config = new Configuration().configure();
SessionFactory sessionFactory = config.buildSessionFactory();
Session session = sessionFactory.openSession();
....
session.close();
透过Session,可以对数据库进行新增、删除、更新,例如使用save()新增一笔数据:
User user = new User();
user.setName("momor");
user.setAge(new Integer(26));
session.save(user);
使用get()或load()方法取得id为1的数据:
User user = (User) session.get(User.class, new Integer(1));
如果未能发现相符合的数据,则get()方法会返回null,而load()方法会丢出ObjectNotFoundException,在进阶的应用中,load()方法可以返回代理对象,并可充分利用缓冲机制。
在Hibernate 3中,取消了find()方法,您必须透过Query或Criteria来进行数据查询。
接下来看看使用Session更新与删除数据,可使用delete()删除数据:
User user = (User) session.get(User.class, new Integer(1));
session.delete(user);
如果您开启了一个Session,从数据表中取出数据显示到使用者接口上,之后关闭Session,当使用者在接口上操作完毕并按下储存时,这时您要重新开启一个Session,使用update()方法将对象中的数据更新至对应的数据表中:
User user = (User) session.get(User.class, new Integer(2));
session.close();
....
user.setAge(new Integer(27));
session = sessionFactory.openSession();
Transaction tx= session.beginTransaction();
session.update(user);
tx.commit();
session.close();
Session提供了一个saveOrUpdate()方法,为数据的储存或更新提供了一个统一的操作接口,藉由定义映像文件时,设定标签的unsaved-value来决定什么是新的值必需,什么是已有的值必须更新:



unsaved-value可以设定的值包括:
• any:总是储存
• none:总是更新
• null:id为null时储存(预设)
• valid:id为null或是指定值时储存
这样设定之后,您可以使用Session的saveOrUpdate()方法来取代update()方法。
Session 管理
Session是Hibernate运作的中心,对象的生命周期、事务的管理、数据库的存取,都与Session息息相关,就如同在编写JDBC时需关心 Connection的管理,以有效的方法创建、利用与回收Connection,以减少资源的消耗,增加系统执行效能一样,有效的Session管理,也是Hibernate应用时需关注的焦点。
Session是由SessionFactory所创建,SessionFactory是执行绪安全的(Thread-safe),您可以让多个执行绪同时存取SessionFactory而不会有数据共享的问题,然而Session则不是设计为执行绪安全的,所以试图让多个执行绪共享一个 Session,将会发生数据共享而发生混乱的问题。
在Hibernate参考手册中的 Quickstart with Tomcat 中,示范了一个HibernateUtil,它使用了ThreadLocal类别来建立一个Session管理的辅助类,这是Hibernate的Session管理一个广为应用的解决方案,ThreadLocal是*Thread-Specific Storage 模式*的一个运作实例。
由于Thread-Specific Stroage模式可以有效隔离执行绪所使用的数据,所以避开Session的多执行绪之间的数据共享问题,以下列出Hibernate参考手册中的HibernateUtil类:
HibernateUtil.java
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.*;
import org.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) {
// Make sure you log the exception, as it might be swallowed
log.error("Initial SessionFactory creation failed.", ex);
throw new ExceptionInInitializerError(ex);
}
}

public static final ThreadLocal session = new ThreadLocal();
public static Session currentSession() {
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() {
Session s = (Session) session.get();

if (s != null) {
s.close();
}

session.set(null);
}
}
在同一个执行绪中,Session被暂存下来了,但无须担心数据库连结Connection持续占用问题,Hibernate会在真正需要数据库操作时才(从连接池中)取得Connection。
在程序中可以这么使用HibernateUtil:
Session session = HibernateUtil.currentSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());
HibernateUtil.closeSession();
在Web应用程序中,可以藉助Filter来进行Session管理,在需要的时候开启Session,并在Request结束之后关闭Session,这个部份,在 JavaWorld 技术论坛 的 Wiki 上有篇 在filter中关闭session 可以参考。
Criteria 基本查询
Criteria对SQL进行封装,让开发人员可以用对象的方式来对数据库进行操作,例如下面的查询User表格中的所有数据:
Criteria criteria = session.createCriteria(User.class);
// 查询user所有字段
List users = criteria.list();
Iterator iterator = users.iterator();
System.out.println("id \t name/age");
while(iterator.hasNext()) {
User user = (User) iterator.next();
System.out.println(user.getId() +
" \t " + user.getName() +
"/" + user.getAge());
}
Hibernate实际上使用以下的SQL来查询数据库:
select this_.id as id0_, this_.name as name0_0_, this_.age as age0_0_ from user this_
Criteria实际上只是个容器,如果想要设定查询条件,则要使用add()方法加入Restrictions的条件限制,例如查询age大于20且小于40的数据:
Criteria criteria = session.createCriteria(User.class);
criteria.add(Restrictions.gt("age", new Integer(20)));
criteria.add(Restrictions.lt("age", new Integer(40)));
List users = criteria.list();
您也可以使用逻辑组合来进行查询,例如结合age等于(eq)20或(or)age为空(isNull)的条件:
Criteria criteria = session.createCriteria(User.class);
criteria.add(Restrictions.or(
Restrictions.eq("age", new Integer(20)),
Restrictions.isNull("age")
));
List users = criteria.list();
也可以使用sqlRestriction()方法来提供SQL语法作限定查询,例如查询name以cater开头的数据:
Criteria criteria = session.createCriteria(User.class);
criteria.add(Restrictions.sqlRestriction("{alias}.name LIKE (?)", "cater%", Hibernate.STRING));
List users = criteria.list();
其中alias将被替换为与User类别相关的名称,而?将被替换为cater%,也就是第二个参数所提供的值,在SQL撰写时,不必再写WHERE,如果有多个查询条件,例如BETWEEN子句的查询,则可以如下:
Criteria criteria = session.createCriteria(User.class);
Integer[] ages = {new Integer(20), new Integer(40)};
Type[] types = {Hibernate.INTEGER, Hibernate.INTEGER};
criteria.add(Restrictions.sqlRestriction("{alias}.age BETWEEN (?) AND (?)", ages, types));
List users = criteria.list();
Restrictions的几个常用限定查询方法如下表所示:
方法 说明
Restrictions.eq 等于
Restrictions.allEq 使用Map,使用key/value进行多个等于的比对
Restrictions.gt 大于 >
Restrictions.ge 大于等于 >=
Restrictions.lt 小于 <
Restrictions.le 小于等于 <=
Restrictions.between 对应SQL的BETWEEN子句
Restrictions.like 对应SQL的LIKE子句
Restrictions.in 对应SQL的in子句
Restrictions.and and关系
Restrictions.or or关系
Restrictions.sqlRestriction SQL限定查询
Criteria 进阶查询
您可以使用Criteria进行查询,并使用Order对结果进行排序,例如使用Oder.asc()由小到大排序(反之则使用desc()):
Criteria criteria = session.createCriteria(User.class);
criteria.addOrder(Order.asc("age"));
List users = criteria.list();
setMaxResults()方法可以限定查询回来的笔数,如果配合setFirstResult()设定传回查询结果第一笔数据的位置,就可以实现简单的分页,例如传回第51笔之后的50笔数据(如果有的话):
Criteria criteria = session.createCriteria(User.class);
criteria.setFirstResult(51);
criteria.setMaxResult(50);
List users = criteria.list();
您可以对查询结果进行统计动作,使用Projections的avg()、rowCount()、count()、max()、min()、countDistinct()等方法,例如对查询结果的"age"作平均:
Criteria criteria = session.createCriteria(User.class);
criteria.setProjection(Projections.avg("age"));
List users = criteria.list();
Iterator iterator = users.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
还可以配合Projections的groupProperty()来对结果进行分组,例如以"age"进行分组,也就是如果数据中"age"如果有20、20、25、30,则以下会显示20、25、30:
Criteria criteria = session.createCriteria(User.class);
criteria.setProjection(Projections.groupProperty("age"));
List users = criteria.list();
Iterator iterator = users.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
如果想结合统计与分组功能,则可以使用ProjectionList,例如下面的程序会计算每个年龄各有多少个人:
ProjectionList projectionList = Projections.projectionList();
projectionList.add(Projections.groupProperty("age"));
projectionList.add(Projections.rowCount());

Criteria criteria = session.createCriteria(User.class);
criteria.setProjection(projectionList);
List users = criteria.list();
Iterator iterator = users.iterator();
while(iterator.hasNext()) {
Object[] o = (Object[]) iterator.next();
System.out.println(o[0] + "\t" + o[1]);
}
如果有一个已知的对象,则可以根据这个对象作为查询的依据,看看是否有属性与之类似的对象,例如:
User user = new User();
user.setAge(new Integer(30));
Criteria criteria = session.createCriteria(User.class);
criteria.add(Example.create(user));

List users = criteria.list();
Iterator iterator = users.iterator();
System.out.println("id \t name/age");
while(iterator.hasNext()) {
User ur = (User) iterator.next();
System.out.println(ur.getId() +" \t " + ur.getName() +
"/" + ur.getAge());
}
在这个例子中,user对象中有已知的属性"age"为30,使用Example会自动过滤掉user的空属性,并以之作为查询的依据,也就是找出"age"同为30的资料。
Criteria可以进行复合查询,即在原有的查询基础上再进行查询,例如在Room对User的一对多关联中,在查询出所有的Room数据之后,希望再查询users中"age"为30的user资料:
Criteria roomCriteria = session.createCriteria(Room.class);
Criteria userCriteria = roomCriteria.createCriteria("users");
userCriteria.add(Restrictions.eq("age", new Integer(30)));
List rooms = roomCriteria.list(); // 只列出users属性中有user之"age"为30的Room
Iterator iterator = rooms.iterator();
DetchedCriteria(新)
Criteria与Session绑定,其生命周期跟随着Session结束而结束,使用Criteria时进行查询时,每次都要于执行时期动态建立对象,并加入各种查询条件,随着Session的回收,Criteria也跟着回收。
为了能够重复使用Criteria对象,在Hibernate 3中新增了DetchedCriteria,您可以先建立DetchedCriteria实例,并加入各种查询条件,并于需要查询时再与Session绑定,获得一个绑定Session的Criteria对象,例如:
// 先建立DetchedCriteria对象
DetachedCriteria detchedCriteria = DetachedCriteria.forClass(User.class);
// 加入查询条件
detchedCriteria.add(Restrictions.ge("age",new Integer(25)));

Session session = sessionFactory.openSession();
// 绑定Session并返回一个Criteria实例
Criteria criteria = detchedCriteria.getExecutableCriteria(session);

List users = criteria.list();
Iterator iterator = users.iterator();
System.out.println("id \t name/age");
while(iterator.hasNext()) {
User ur = (User) iterator.next();
System.out.println(ur.getId() +
" \t " + ur.getName() +
"/" + ur.getAge());
}
Query
可以透过org.hibernate.Query接口的实例来进行查询,透过Query接口,您可以先设定查询参数,之后透过setXXX()等方法,将指定的参数值填入,而不用每次都撰写完整的HQL,直接来看个例子:
Session session = sessionFactory.openSession();
Query query = session.createQuery("select user.name from User as user where user.age > ?");
query.setInteger(0, 25);

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
session.close();
在设定参数值时,必须依照 ? 所设定的顺序,并使用对应型态的setXXX()方法,一个执行的例子如下:
Hibernate: select user0_.name as col_0_0_ from user user0_ where user0_.age>?
momor
caterpillar
bush
您可以使用命名参数(Named Parameter)来取代这个方法,这可以不用依照特定的顺序来设定参数值,并拥有较好的可读性,直接来看个例子:
Session session = sessionFactory.openSession();
Query query = session.createQuery("select user.name from User as user where user.age > :minAge");
query.setInteger(":minAge", 25);

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
session.close();
设定命名参数时,在建立Query时先使用:后跟着参数名,之后就可以在setXXX()方法中直接指定参数名来设定参数值,而不用依照特定的顺序。
也可以将HQL撰写在程序之外,以避免硬编码(Hard code)在程序之中,在需要修改HQL时就很方便,在*.hbm.xml中使用标签,并在之间撰写HQL,撰写的位置是在之前,例如:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">









select user.name from User as user where user.age > :minAge
]]>


的name属性用来设定查询外部HQL时的名称依据,使用的例子如下:
Session session = sessionFactory.openSession();
Query query = session.getNamedQuery("onlyfun.caterpillar.QueryUser");
query.setInteger("minAge", 25);

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}

session.close();
HQL(Hibernate Query Language)
基本查询(新)
用HQL查询最简单的例子,就是查询指定类别对应表格的所有数据,例如:
Session session = sessionFactory.openSession();
Query query = session.createQuery("from User");

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
User user = (User) iterator.next();
System.out.println(user.getId() + "\t" +
user.getAge() + "\t" +
user.getName());
}
也可以指定类别的全名,例如:
Query query = session.createQuery("from onlyfun.caterpillar.User");
HQL本身不区分大小写,不过要注意类别的名称必须区分大小写。
在查询类别对应的表格时,需注意到继承的问题,Hibernate会自动判定继承关系,如果查询的类别是某类别的父类别,则会返回与父类别、子类别对应的所有表格数据,例如如果查询java.lang.Object,由于Object在Java中是所有类别的父类别,所以下面这个查询会返回数据库中所有表格的数据:
Query query = session.createQuery("from java.lang.Object");
如果要针对某个属性作查询,则可以如下:
Session session = sessionFactory.openSession();
Query query = session.createQuery("select user.name from User as user");

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
如果要查询两个以上的属性,则如下,查询的结果会以数组的方式传回:
Session session = sessionFactory.openSession();
Query query = session.createQuery("select user.age, user.name from User as user");

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
Object[] obj = (Object[]) iterator.next();
System.out.println(obj[0] + "\t" + obj[1]);
}
如果User类别提供有适当的建构方法,则可以在使用HQL时直接指定新建一个对象传回,例如若User如下设计:
package onlyfun.caterpillar;
public class User {
private Integer id;
private String name;
private Integer age;

public User() {
}

public User(String name, Integer age) {
this.name = name;
this.age = age;
}
}
则在使用HQL查询时可以如下:
Session session = sessionFactory.openSession();
Query query = session.createQuery("select new User(user.name, user.age) from User as user");

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
User user= (User) iterator.next();
System.out.println(user.getAge() + "\t" + user.getName());
}
要注意的是,这个返回的User实例并未与数据库有任何关联,可以试着取得id属性,可以发现它的值是nul,如果试图使用Session的saveOrupdate()方法,则会新增一笔数据而不是更新原有的数据。
可以使用distinct去除数据重复的记录:
Query query = session.createQuery("select distinct user.age from User as user");
List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
您也可以在HQL中使用函式,例如取得资料的笔数:
Query query = session.createQuery("select count(*) from User as user");

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
使用avg()取得属性的平均值:
Query query = session.createQuery("select avg(user.age) from User as user");

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
使用upper()函式将字符串转为大写:
Query query = session.createQuery("select upper(user.name) from User as user");

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
where、group by、order by 子句
可以使用where子句来限定查询的条件,除了 = 运算之外,还有 >、>=、<、<=、!= 或 <>等比较运算,例如:
Session session = sessionFactory.openSession();
Query query = session.createQuery("from User user where user.name='caterpillar'");

List names = query.list();
Iterator iterator = names.iterator();
while(iterator.hasNext()) {
User user = (User) iterator.next();
System.out.println(user.getAge() + "\t" + user.getName());
}
也可以在where子句上进行表达式,例如:
Query query = session.createQuery("from User user where (user.age / 10 = 3)");
也可以在where子句上使用and、or,例如:
Query query = session.createQuery("from User user where (user.age > 20) and (user.name = 'caterpillar')");
is not nullL与is null则可以测试字段值是否为空值,例如:
Query query = session.createQuery("from User user where user.name is not null");
between可以测试字段值是否在指定的范围之内,例如:
Query query = session.createQuery("from User user where user.age between 20 and 30");
可以使用in或not in来测试字段值是否在您指定的集合中,例如:
Query query = session.createQuery("from User user where user.name in('caterpillar', 'momor')");
like或not like可以让您进行模糊条件搜寻,例如想搜寻名称中含有cater开头的数据:
Query query = session.createQuery("from User user where user.name like 'cater%'");
可以对查询结果使用order by进行排序:
Query query = session.createQuery("from User user order by user.age");
可使用desc反排序:
Query query = session.createQuery("from User user order by user.age desc");
可同时指定两个以上的排序方式,例如先按照"age"反序排列,如果"age"相同,则按照"name"顺序排列:
Query query = session.createQuery("from User user order by user.age desc, user.name");
可以配合GROUP BY子句,自动将指定的字段依相同的内容群组,例如依字段"sex"分组并作平均:
Query query = session.createQuery("select user.sex, avg(user.age) from User user group by user.sex");
一个执行的结果如下:
+-------------------------------+
| sex | avg(age) |
+-------------------------------+
| male | 30 |
+-------------------------------+
| female | 25 |
+-------------------------------+
还可以结合having子句,例如只将平均大于20的数据分组显示出来:
Query query = session.createQuery("select user.sex, avg(user.age) from User user group by user.sex having avg(user.age) > 20");
更新、删除(新)
在Hibernate 2时,HQL只用于查询数据,要更新或删除数据,则是依赖于Session的update()、saveOrUpdate()、delete()等方法,在Hibernate 3中,HQL新增了update与delete语句,可以直接使用HQL指定更新或删除,例如使用update子句进行更新:
Session session = sessionFactory.openSession();
Transaction tx= session.beginTransaction();
Query query = session.createQuery("update User set name='momor' where name='bbb'");
query.executeUpdate();
tx.commit();
session.close();
使用delete子句进行数据删除:
Session session = sessionFactory.openSession();
Transaction tx= session.beginTransaction();
Query query = session.createQuery("delete User where name='bush'");
query.executeUpdate();
tx.commit();
session.close();
SQL 支援
建立 SQL 查询
Hibernate提供了对SQL的支持,您可以指定您所要建立的SQL,并将实体类别与数据表格关联,举个例子来说,如果您打算使用像以下的SQL语句:
SELECT * FROM user WHERE age > 20
则您可以如下建立SQL查询:
// SQL,并指定别名为user
String sql = "select {user.*} from User user where user.age > 20";
Session session = sessionFactory.openSession();
// 建立 SQLQuery
SQLQuery sqlQuery = session.createSQLQuery(sql);
// 将别名user与实体类User关联在一起
sqlQuery.addEntity("user", User.class);
Iterator iterator = sqlQuery.list().iterator();
while(iterator.hasNext()) {
User user = (User) iterator.next();
System.out.println(user.getAge() + "\t" + user.getName());
}

session.close();
addEntity()是将实体类别与别名连结在一起的方法,大括号指定要查询的数据,Hibernate根据所给定的SQL自动生成以下的句子:
select user.id as id0_, user.name as name0_0_, user.age as age0_0_ from User user where user.age > 20
返回的结果则由Hibernate进行封装为所指定别名关联之实体类,如此您可以得到使用SQL的弹性,但无需处理繁琐的ResultSet。
您也可以将SQL语句定义在映像文件中,例如:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">


select {user.*} from User user where user.age > 20
]]>



定义的时候,使用卷标指定别名与实体类之关联,配合映像文件中的定义,您可以如下运行Hibernate:
Session session = sessionFactory.openSession();
Query query = session.getNamedQuery("onlyfun.caterpillar.QueryUser");

Iterator iterator = query.list().iterator();
while(iterator.hasNext()) {
User user = (User) iterator.next();
System.out.println(user.getAge() + "\t" + user.getName());
}

session.close();
也可以设定查询参数,例如:
....

select {user.*} from User user where user.age > :age
]]>


....
使用Hibernate查询时如下:
Session session = sessionFactory.openSession();
Query query = session.getNamedQuery("onlyfun.caterpillar.QueryUser");
query.setInteger("age", 20);

Iterator iterator = query.list().iterator();
while(iterator.hasNext()) {
User user = (User) iterator.next();
System.out.println(user.getAge() + "\t" + user.getName());
}

session.close();

自定义 insert、update、delete(新)
Hibernate 3的映射文件中新增了三个标签,您可以在这三个标签中使用SQL自定义您的INSERT、UPDATE、DELETE,也就是储存、更新、删除数据时的行为,例如:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">









INSERT INTO user (name, age) VALUES (?, ?)


UPDATE user SET name=?, age=?, WHERE id=?


DELETE FROM user WHERE id=?



? (参数) 对应的顺序是映像文件中属性出现的顺序,假设您储存对象:
session = sessionFactory.openSession();
tx = session.beginTransaction();

session.save(user);
tx.commit();
session.close();
则执行结果中显示的SQL语句会是您自定义的语句,而不是由Hibernate自动生成的语句:
Hibernate:
INSERT INTO user (name, age) VALUES (?, ?)

映射基础议题
实体对象生命周期
Hibernate中的实体对象可以分为三种状态:Transient、Persistent、Detached。
• Transient
当您直接使用new创建出对象,例如在之前的例子中,User类别所衍生出之对象,在还没有使用save()之前都是暂存对象,这些对象还没有与数据库发生任何的关系,不对应于数据库中的任一笔数据。
• Persistent
当对象与数据库中的数据有对应关系,并且与Session实例有关联而Session 实例尚未关闭(close),则它是在Persistent状态,具体而言,如果您将Transient状态的对象使用Session的save()方法加以储存,或是使用Hibernate从数据库加载数据并封装为对象(例如使用get()、load()),则该对象为Persistent状态。
Persistent状态的对象对应于数据库中的一笔数据,对象的id值与数据的主键值相同,并且Session实例尚未失效,在这期间您对对象的任何状态变动,在Session实例关闭(close)或Transaction实例执行commit()之后,数据库中对应的数据也会跟着更新。
如果您将Session实例关闭(close),则Persistent状态的对象会成为Detached状态。
如果您使用Session的实例delete()方法删除数据,Persistent状态的对象由于失去了对应的数据,则它会成为Transient状态。
• Detached
Detached状态的对象,其id与数据库的主键值对应,但脱离Session实例的管理,例如在使用load()方法查询到数据并封装为对象之后,将Session实例关闭,则对象由Persistent状态变为Detached状态,Detached状态的对象之任何属性变动,不会对数据库中的数据造成任何的影响。Detached状态的对象可以使用update()方法使之与数据库中的对应数据再度发生关联,此时Detached状态的对象会变为Persistent状态。
简单的说,Transient与Detached状态的对象未受Hibernate持久层管理员管理,对这两个状态的对象作任何属性变动,不会对数据库中的数据有任何的影响,而Persistent状态的对象受Hibernate持久层管理,对对象的属性变动,在Session实例关闭(close)或 Transaction实例执行commit()之后,数据库中对应的数据也会跟着更新。
Transient与Detached状态的对象是非管理状态,而Persistent状态的对象是管理状态,又称为Persistent Object。
在对象为Persistent时,如果对象的属性发生变化,并且尚未提交之前,对象所携带的数据称之为Dirty Data,Hibernate会在持久层维护对象的最近读取版本,并在数据提交时检查两个版本的属性是否有变化,如果有的话,则将数据库中的数据进行更新。
数据识别(Data Identity)
讨论一下对象识别问题,对数据库而言,其识别一笔数据唯一性的方式是根据主键值,如果手上有两份数据,它们拥有同样的主键值,则它们在数据库中代表同一个字段的数据。对Java而言,要识别两个对象是否为同一个对象有两种方式,一种是根据对象是否拥有同样的内存位置来决定,在Java语法中就是透过== 运算来比较,一种是根据equals()、hasCode()中的定义。
先探讨第一种Java的识别方式在Hibernate中该注意的地方,在Hibernate中,如果是在同一个session中根据相同查询所得到的相同数据,则它们会拥有相同的Java识别,举个实际的例子来说明:
Session session = sessions.openSession();
Object obj1 = session.load(User.class, new Integer(1));
Object obj2 = session.load(User.class, new Integer(1));
session.close();

System.out.println(obj1 == obj2);
上面这个程序片段将会显示true的结果,表示obj1与obj2是参考至同一对象,但如果是以下的情况则不会显示false:
Session session1 = sessions.openSession();
Object obj1 = session1.load(User.class, new new Integer(1));
session1.close();

Session session2 = sessions.openSession();
Object obj1 = session2.load(User.class, new Integer(1));
session2.close();

System.out.println(obj1 == obj2);
原因可以参考 简介快取(Session Level) 。
Hibernate并不保证不同时间所取得的数据对象,其是否参考至内存的同一位置,使用==来比较两个对象的数据是否代表数据库中的同一笔数据是不可行的,事实上就算在Java程序中也不会这么比较两个对象是否相同,要比较两个对象是否相同,会透过equals()方法,而Object预设的 equals()本身即比较对象的内存参考,如果您要有必要比较透过查询后两个对象的数据是否相同(例如当对象被储存至Set时)您必须实作 equals()与hashCode()。
一个实作equals()与hashCode()的方法是根据数据库的identity,方法之一是透过getId()方法取得对象的id值并加以比较,例如若id的型态是String,一个实作的例子如下:
public class User {
....

public boolean equals(Object o) {
if(this == o) return true;
if(id == null || !(o instanceof User)) return false;

final User user == (User) o;
return this.id.equals(user.getId());
}

public int hasCode() {
return id == null ? System.identityHashCode(this) : id.hashcode();
}

}
这个例子取自于Hibernate in Action第123页的范例,然而这是个不被鼓励的例子,因为当一个对象被new出来而还没有save()时,它并不会被赋予id值,这时候就不适用这个方法。
另一个比较被采用的方法是根据对象中真正包括的的属性值来作比较,在 Hibernate官方参考手册 中给了一个例子:
public class Cat {

...
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof Cat)) return false;

final Cat cat = (Cat) other;

if (!getName().equals(cat.getName())) return false;
if (!getBirthday().equals(cat.getBirthday())) return false;

return true;
}

public int hashCode() {
int result;
result = getName().hashCode();
result = 29 * result + getBirthday().hashCode();
return result;
}

}
这个例子不是简单的比较id属性,而是一个根据商务键值(Business key)实作equals()与hasCode()的例子,当然留下的问题就是您如何在实作时利用相关的商务键值,这就要根据您实际的商务需求来决定了。
愿意的话,还可以使用org.apache.commons.lang.builder.EqualsBuilder与 org.apache.commons.lang.builder.HashCodeBuilder来协助定义equals()与hashCode(),例如:
package onlyfun.caterpillar;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

public class User {
....
public boolean equals(Object obj) {
if(obj == this) {
return true;
}

if(!(obj instanceof User)) {
return false;
}

User user = (User) obj;
return new EqualsBuilder()
.append(this.name, user.getName())
.append(this.phone, user.getPhone())
.isEquals();

}

public int hashCode() {
return new HashCodeBuilder()
.append(this.name)
.append(this.phone)
.toHashCode();
}
}

实体映像
复合主键
基于业务需求,您会需要使用两个字段来作复合主键,例如在User数据表中,您也许会使用"name"与"phone"两个字段来定义复合主键。
假设您这么建立User表格:
CREATE TABLE user (
name VARCHAR(100) NOT NULL,
phone VARCHAR(50) NOT NULL,
age INT,
PRIMARY KEY(name, phone)
);
在表格中,"name"与"age"被定义为复合主键,在映像时,您可以让User类别直接带有"name"与"age"这两个属性,而Hibernate要求复合主键类别要实作Serializable接口,并定义equals()与hashCode()方法:
User.java
package onlyfun.caterpillar;

import java.io.Serializable;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

// 复合主键类的对应类别必须实作Serializable接口
public class User implements Serializable {
private String name;
private String phone;
private Integer age;

public User() {
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getPhone() {
return phone;
}

public void setPhone(String phone) {
this.phone = phone;
}

// 必须重新定义equals()与hashCode()
public boolean equals(Object obj) {
if(obj == this) {
return true;
}

if(!(obj instanceof User)) {
return false;
}

User user = (User) obj;
return new EqualsBuilder()
.append(this.name, user.getName())
.append(this.phone, user.getPhone())
.isEquals();

}

public int hashCode() {
return new HashCodeBuilder()
.append(this.name)
.append(this.phone)
.toHashCode();
}
}
equals()与hashCode()方法被用作两笔不同数据的识别依据;接着您可以使用在映射文件中定义复合主键与对象的属性对应:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">





column="name"
type="java.lang.String"/>
column="phone"
type="java.lang.String"/>






在储存数据方面,复合主键的储存没什么区别,现在的问题在于如何依据复合主键来查询数据,例如使用load()方法,您可以创建一个User实例,并设定复合主键对应的属性,接着再透过load()查询对应的数据,例如:
User user = new User();
user.setName("bush");
user.setPhone("0970123456");

Session session = sessionFactory.openSession();
// 以实例设定复合主键并加载对应的数据
user = (User) session.load(User.class, user);

System.out.println(user.getAge() + "\t" +
user.getName() + "\t" +
user.getPhone());
session.close();
可以将主键的信息独立为一个类别,例如:
UserPK.java
package onlyfun.caterpillar;

import java.io.Serializable;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

public class UserPK implements Serializable {
private String name;
private String phone;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getPhone() {
return phone;
}

public void setPhone(String phone) {
this.phone = phone;
}

public boolean equals(Object obj) {
if(obj == this) {
return true;
}

if(!(obj instanceof User)) {
return false;
}

UserPK pk = (UserPK) obj;
return new EqualsBuilder()
.append(this.name, pk.getName())
.append(this.phone, pk.getPhone())
.isEquals();

}

public int hashCode() {
return new HashCodeBuilder()
.append(this.name)
.append(this.phone)
.toHashCode();
}
}
现在User类别的主键信息被分离出来了,例如:
User.java
package onlyfun.caterpillar;

import java.io.Serializable;

public class User implements Serializable {
private UserPK userPK; // 主键
private Integer age;

public User() {
}

public UserPK getUserPK() {
return userPK;
}

public void setUserPK(UserPK userPK) {
this.userPK = userPK;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}
在映像文件方面,需要指定主键类的信息,例如:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">




class="onlyfun.caterpillar.UserPK"
unsaved-value="any">
column="name"
type="java.lang.String"/>
column="phone"
type="java.lang.String"/>







在查询数据时,必须指定主键信息,例如:
UserPK pk = new UserPK();
pk.setName("bush");
pk.setPhone("0970123456");

Session session = sessionFactory.openSession();
// 以主键类实例设定复合主键并加载对应的数据
User user = (User) session.load(User.class, pk);

System.out.println(user.getAge() + "\t" +
user.getUserPK().getName() + "\t" +
user.getUserPK().getPhone());
session.close();

Blob、Clob
在Hibernate中,您可以直接对Blob、Clob作映像,例如在MySQL中,您的数据库表格若是这么建立的:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
age INT,
photo BLOB,
resume TEXT
);
您可以定义一个User类别,并让属性包括java.sql.Blob与java.sql.Clob,如下:
User.java
package onlyfun.caterpillar;

import java.sql.Blob;
import java.sql.Clob;

public class User {
private Integer id;
private String name;
private Integer age;
private Blob photo;
private Clob resume;

// 必须要有一个预设的建构方法
// 以使得Hibernate可以使用Constructor.newInstance()建立对象
public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public Blob getPhoto() {
return photo;
}

public void setPhoto(Blob photo) {
this.photo = photo;
}

public Clob getResume() {
return resume;
}

public void setResume(Clob resume) {
this.resume = resume;
}
}
接着在映射文件中,可以如下定义:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">












在进行数据储存时,可以使用Hibernate.createBlob()与Hibernate.createClob()从来源数据建立Blob与Clob实例,例如:
FileInputStream fileInputStream = new FileInputStream("c:\\workspace\\photo.jpg");
Blob photo = Hibernate.createBlob(fileInputStream);
Clob resume = Hibernate.createClob("Bla....Bla....resume text!!");

User user = new User();
user.setName("caterpillar");
user.setAge(new Integer(30));
user.setPhoto(photo);
user.setResume(resume);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user);
tx.commit();
session.close();
如果打算从数据库中取得数据,则一个范例如下所示:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.print(user.getAge() + "\t" +
user.getName() + "\t");
String resume = user.getResume().getSubString(1, (int) user.getResume().length());
System.out.println(resume);

// 将Blob数据写到档案
InputStream inputStream = user.getPhoto().getBinaryStream();
FileOutputStream fileOutputStream = new FileOutputStream("c:\\workspace\\photo_save.jpg");
byte[] buf = new byte[1];
int len = 0;
while((len = inputStream.read(buf)) != -1) {
fileOutputStream.write(buf, 0, len);
}
inputStream.close();
fileOutputStream.close();

System.out.println("save photo to c:\\workspace\\photo_save.jpg");

session.close();
在MySQL中对Blob与Clob是比较简单的,如果在Oracle DB中则复杂一些,您可以参考 Using Clobs/Blobs with Oracle and Hibernate。
Component
假设您设计了这么一个user表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
age INT,
email VARCHAR(100) NOT NULL
);
最基本的映像策略中,您可以设计一个如下的User类别与之对应:
package onlyfun.caterpillar;

public class User {
private Integer id;
private String name;
private int age;
private String email;

........
}
现在假设您基于业务上的设计需求,您需要将email信息提升为一个MailAddress对象,让它携带更多信息或负有特定职责,例如:
MailAddress.java
package onlyfun.caterpillar;

public class MailAddress {
private String email;

public MailAddress() {
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public void sendMail() {
System.out.println("Send mail to " + email);
}
}
而User类别中有(has a)MailAddress,例如:
User.java
package onlyfun.caterpillar;

public class User {
private Integer id;
private String name;
private Integer age;
private MailAddress mailAddress;

// 必须要有一个预设的建构方法
// 以使得Hibernate可以使用Constructor.newInstance()建立对象
public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public MailAddress getMailAddress() {
return mailAddress;
}

public void setMailAddress(MailAddress mailAddress) {
this.mailAddress = mailAddress;
}
}
在数据库表格方面并没有任何的改变,这是基于程序设计上的考虑,增加对象设计上的粒度,MailAddress为User的一个Component,在映射文件上,您可以使用标签来完成这样的映像:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">









column="email"
type="java.lang.String"
not-null="true"/>



在对象储存时的一个示范如下:
MailAddress mailAddress = new MailAddress();
mailAddress.setEmail("[email protected]");

User user = new User();
user.setName("caterpillar");
user.setAge(new Integer(30));
user.setMailAddress(mailAddress);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user);
session.flush();
tx.commit();
session.close();
在对象查询与使用上的一个例子如下:
Session session = sessionFactory.openSession();

User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getAge() + "\t" +
user.getName() + "\t" +
user.getMailAddress().getEmail());
user.getMailAddress().sendMail();

session.close();
动态模型(Dynamic Model)
在构造系统原型(Prototype)阶段时,由于需求尚未确定,应用程序模型中的Java对象会有相当大的变动,在Hibernate 3中引入了动态模式,可以使用对象容器充当Java对象,在构造系统原型时灵活变化,而不必实际定义Java对象。
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
age INT
);
使用动态模式来作映像时,无需定义Java对象,直接在映像文件的卷标上使用entity-name属性:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">


table="user">



column="name"
type="java.lang.String"/>
column="age"
type="java.lang.Integer"/>


entity-name属性设定的名称将在储存或加载时使用,例如可以如下储存数据:
Map userMap = new HashMap();
userMap.put("name", "caterpillar");
userMap.put("age", new Integer(30));

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save("onlyfun.caterpillar.DynamicUserModel", userMap);
tx.commit();
session.close();
Map容器的key用来表示属性名称,而value用来表示储存之对象,它们将对应至数据表中的字段与值,上面的程序片段储存数据后,数据表内容如下:
mysql> select * from user;
+----+-------------+------+
| id | name | age |
+----+-------------+------+
| 1 | caterpillar | 30 |
+----+-------------+------+
1 row in set (0.00 sec)
如果要加载数据,范例如下所示:
Session session = sessionFactory.openSession();
Map userMap = (Map) session.load("onlyfun.caterpillar.DynamicUserModel", new Integer(1));
System.out.println(userMap.get("name"));
System.out.println(userMap.get("age"));
session.close();
Hibernate 3引入动态模型的目的,在于更灵活的构造原型系统,在系统架构与对象模式确定之后,仍是要设计专用的Java对象,以获得编译时期的型态检查等好处。
继承映射
继承 - Table per concrete class
如果您采取的是对象模型的方式来设计程序,那么继承关系可能就会在您的程序设计中出现,然而关系型数据库的关联模型与对象模型并不匹配,为了映像对象模型 与关联模型,您可以采取三种策略,这边先介绍最简单的一种:Table per concrete class,也就是每一个类别就对应一个表格。
以实例来说明,如果您的程序中有以下的继承关系:

以Table per concrete class的方式在关系型数据库中设计表格以作映像,就是直接对DefaultUser与PowerUser设计两个表格与之对应,如下:

两个表格没有任何的关系,defaultuser与poweruser表格各自拥有id、name字段,您可以如下建立表格:
CREATE TABLE defaultuser (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
someProperty VARCHAR(100)
);

CREATE TABLE poweruser (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
otherProperty VARCHAR(100)
);
User.java
package onlyfun.caterpillar;

public class User {
private Integer id;
private String name;

// 必须要有一个预设的建构方法
// 以使得Hibernate可以使用Constructor.newInstance()建立对象
public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}
DefaultUser.java
package onlyfun.caterpillar;

public class DefaultUser extends User {
private String someProperty;

public DefaultUser() {
}

public String getSomeProperty() {
return someProperty;
}

public void setSomeProperty(String someProperty) {
this.someProperty = someProperty;
}
}
在映射文件方面分别如下:
DefaultUser.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">










PowerUser.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">










当然,别忘了在hibernate.cfg.xml中定义使用这两个映射文件:
hibernate.cfg.xml

"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">


...





在储存对象时很简单,例如分别储存DefaultUser与PowerUser的实例:
DefaultUser defaultUser = new DefaultUser();
defaultUser.setName("Bush");
defaultUser.setSomeProperty("hu....hu...");

PowerUser powerUser = new PowerUser();
powerUser.setName("caterpillar");
powerUser.setOtherProperty("Bla...Bla...");

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

session.save(powerUser);
session.save(defaultUser);

tx.commit();
session.close();
至于查询方面,视您想查询哪个数据表,如果想同时查询User类对应的所有资料,则可以如下:
Session session = sessionFactory.openSession();
Query query = session.createQuery("from onlyfun.caterpillar.User");
Iterator iterator = query.list().iterator();
while(iterator.hasNext()) {
User user = (User) iterator.next();
System.out.println(user.getName());
}

session.close();
这个查询会找出所有User之子类别所对应之数据,Hibernate实际上会使用以下的SQL进行查询:
Hibernate: select defaultuse0_.id as id, defaultuse0_.name as name0_, defaultuse0_.someProperty as someProp3_0_
from defaultuser defaultuse0_
Hibernate: select poweruser0_.id as id, poweruser0_.name as name1_, poweruser0_.otherProperty as otherPro3_1_
from poweruser poweruser0_
也就是Hibernate自动判断User的继承关系,并分别对DefaultUser与PowerUser对应的表格进行查询。
文件虽然很长,但其实不写这份文件也可以,还记得 基本查询 中曾经提过,如果您直接使用"from java.lang.Object"这样的HQL查询,所得到的结果是数据库中所有的数据,因为Object是Java中所有类别的父类别,写这份文件只是在加强您的印象,了解继承关系映像最基本的方式而已。
父类别User对应的表格呢?这边没有建立,如果需要的话,就自己再建立一个吧!并撰写对应的映射文件就可以了。
继承 - Table per class hierarchy
接续 上一个主题,Table per concrete class的继承映像方式是最简单,但没有效率(例如查询同为User类型时,需要两次SQL)且不易管理的映像方式,来看看继承关系映像至关系型数据库 的第二种方式:Table per class hierarchy。
这种方式使用一个表格储存同一个继承阶层的所有类别,并使用额外的字段来表示所记录的是哪一个子类别的数据。
具体来说,对于继承User类别的DefaultUser及PowerUser,可以设计以下的表格来储存数据:

可以使用以下的SQL来建立表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
userType VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL default '',
someProperty VARCHAR(100),
otherProperty VARCHAR(100)
);
现在所决定的是,如果要储存的数据是来自DefalutUser,则在userType记下"Default",如果储存的数据来PowerUser,则 在userType记下"Power",由userType就可以在数据从数据库取回时,决定其该封装为DefaultUser或是PowerUser, 在使用Hibernate的话,这要在映射文件中使用等相关标签来定义,例如:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">








discriminator-value="Default">
column="someProperty"
type="java.lang.String"/>


discriminator-value="Power">
column="otherProperty"
type="java.lang.String"/>



当然,别忘了在hibernate.cfg.xml 中指定映像文件:
hibernate.cfg.xml
hibernate.cfg.xml

"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">



....





使用上一个主题中的储存程序的话,则Hibernate会使用以下的SQL来储存数据:
Hibernate: insert into user (name, otherProperty, userType) values (?, ?, 'Power')
Hibernate: insert into user (name, someProperty, userType) values (?, ?, 'Default')
而实际上数据表会储存以下的内容:
+----+----------+-------------+-------------------+-------------------+
| id | userType | name | someProperty | otherProperty |
+----+----------+-------------+-------------------+-------------------+
| 1 | Power | caterpillar | NULL | Bla...Bla... |
| 2 | Default | Bush | hu....hu... | NULL |
+----+----------+-------------+-------------------+-------------------+
缺点就是,因子类别属性的不同,对映储存时会有许多字段没有数据,但查询效率较好,例如查询User类型的数据时,只需一次SQL,如使用 上一个主题 中的查询程序时,Hibernate会使用以下的SQL进行查询:
Hibernate:
select user0_.id as id, user0_.name as name0_,
user0_.someProperty as someProp4_0_, user0_.otherProperty as otherPro5_0_,
user0_.userType as userType from user user0_
继承 - Table per subclass
接续上一个主题,在使用Table per class hierarchy映像继承关系时,会有大量的字段有NULL的情况,好处是使用一个表格,查询时只需一次SQL。
Table per subclass的继承映像方式,给予父类与子类分别的表格,而父类与子类对应的表格透过外键来产生关联,具体的说,User类别、DefaultUser类别与PowerUser类别所映像的表格如下:

其中user表格的id与defaultuser及poweruser的id 一致,具体的说,在储存DefaultUser实例时,id与name属性记录在user表格中,而someProperty记录在 defaultuser中,假设user表格的id值为1,则defaultuser表格对应的该笔记录其id值也会为一。
可以使用以下的SQL建立资料表:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);

CREATE TABLE defaultuser (
id INT(11) NOT NULL PRIMARY KEY,
someProperty VARCHAR(100)
);

CREATE TABLE poweruser (
id INT(11) NOT NULL PRIMARY KEY,
otherProperty VARCHAR(100)
);
在映射文件上,如下定义:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">








table="defaultuser">

column="someProperty"
type="java.lang.String"/>


table="poweruser">

column="otherProperty"
type="java.lang.String"/>



指明了子类别与所对应的表格,指明子类别的对应表格中,哪一个字段要与父类别的主键一致。
使用 继承 - Table per concrete class 中的储存程序片段,则查询表格时可以发现以下的结果:
mysql> select * from user;
+-----+-------------+
| id | name |
+-----+-------------+
| 1 | caterpillar |
| 2 | Bush |
+-----+-------------+
2 rows in set (0.00 sec)

mysql> select * from defaultuser;
+----+-------------------+
| id | someProperty |
+----+-------------------+
| 2 | hu....hu... |
+----+-------------------+
1 row in set (0.00 sec)

mysql> select * from poweruser;
+----+-------------------+
| id | otherProperty |
+----+-------------------+
| 1 | Bla...Bla... |
+----+-------------------+
1 row in set (0.00 sec)
仔细观察一下,看看defaultuser与poweruser表格中的id各自是对应于user表格中的哪笔数据。
容器映射
Set
关于Set的特性,您可以先参考 HashSet、TreeSet 这两篇文件的介绍,这边先介绍当Set中包括的对象为非实体(Entiy)时的映射方式,简单的说,也就是所包括的对象没有对象识别(Identity),只是纯綷的值型态(Value type)对象)。
假设您有一个User类别,当中除了名称属性之外,另一个就是使用者的电子邮件地址,同一个使用者可能有多个不同的邮件地址,所以在User类别中使用 Set对象来加以记录,在这边使用String来记录每一笔邮件地址,为了不允许重复的邮件地址记录,所以使用Set对象,User类别如下:
User.java
package onlyfun.caterpillar;
import java.util.Set;

public class User {
private Integer id;
private String name;
private Set emails;

// 必须要有一个预设的建构方法
// 以使得Hibernate可以使用Constructor.newInstance()建立对象
public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Set getEmails() {
return emails;
}

public void setEmails(Set emails) {
this.emails = emails;
}

public void addEmail(String email) {
this.emails.add(email);
}

public void removeEmail(String email) {
this.emails.remove(email);
}
}
要映像Set容器,您可以使用另一个表格来储存Set容器中的数据,例如您可以分别建立user与email表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);

CREATE TABLE email (
id INT(11) NOT NULL,
address VARCHAR(100) NOT NULL
);
接着定义映像文件,使用标签来定义Set映像:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">










column="address"/>



假设您如下储存对象:
User user1 = new User();
user1.setEmails(new HashSet());
user1.setName("caterpillar");
user1.addEmail("[email protected]");
user1.addEmail("[email protected]");

User user2 = new User();
user2.setEmails(new HashSet());
user2.setName("momor");
user2.addEmail("[email protected]");

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user1);
session.save(user2);
tx.commit();
session.close();
则数据库中的表格储存内容将如下:
mysql> select * from user;
+----+----------------+
| id | name |
+----+----------------+
| 1 | caterpillar |
| 2 | momor |
+----+----------------+
2 rows in set (0.00 sec)

mysql> select * from email;
+----+-------------------------------------------+
| id | address |
+----+-------------------------------------------+
| 1 | [email protected] |
| 1 | [email protected] |
| 2 | [email protected] |
+----+-------------------------------------------+
3 rows in set (0.00 sec)
List
关于List的特性,可以先看一下[ArrayList]、[LinkedList]这两篇文件。
List是有序的结构,所以在储存List容器中的对象时,要一并储存其顺序信息,例如若您设计了以下的类别:
User.java
package onlyfun.caterpillar;

import java.util.List;

public class User {
private Integer id;
private String name;
private List items;

// 必须要有一个预设的建构方法
// 以使得Hibernate可以使用Constructor.newInstance()建立对象
public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List getItems() {
return items;
}

public void setItems(List items) {
this.items = items;
}

public void addItem(String item) {
items.add(item);
}

public void removeItem(String item) {
items.remove(item);
}
}
在设计表格时,使用一个item表格来记录List容器信息,item表格必须包括索引信息,例如您可以如下建立user与item表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);

CREATE TABLE item (
id INT(11) NOT NULL,
position INT(11) NOT NULL,
name VARCHAR(100) NOT NULL default ''
);
其中position字段要用来储存List的索引信息,可以使用卷标如下定义映像文件:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">















假设您如下储存对象:
User user1 = new User();
user1.setItems(new ArrayList());
user1.setName("caterpillar");
user1.addItem("DC");
user1.addItem("CF Card");

User user2 = new User();
user2.setItems(new ArrayList());
user2.setName("momor");
user2.addItem("comics");

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user1);
session.save(user2);
tx.commit();
session.close();
则数据库中的储存状况如下:
mysql> select * from user;
+----+--------------+
| id | name |
+----+--------------+
| 1 | caterpillar |
| 2 | momor |
+----+--------------+
2 rows in set (0.00 sec)

mysql> select * from item;
+----+------------+-----------+
| id | position | name |
+----+------------+-----------+
| 1 | 0 | DC |
| 1 | 1 | CF Card |
| 2 | 0 | comics |
+----+------------+-----------+
3 rows in set (0.00 sec)

Map
Bag
Bag是集合,与Set不同的是,Bag允许重复的元素,在Java的标准API中并没有提供Bag容器,Hibernate提供自己的Bag实现,允许您将List映射为Bag。
您可以如下定义User类别,其中的List成员将被用作Bag来使用,而不管对象在List容器中的顺序:
User.java
package onlyfun.caterpillar;

import java.util.List;

public class User {
private Integer id;
private String name;
private List items;

// 必须要有一个预设的建构方法
// 以使得Hibernate可以使用Constructor.newInstance()建立对象
public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List getItems() {
return items;
}

public void setItems(List items) {
this.items = items;
}

public void addItem(String item) {
items.add(item);
}

public void removeItem(String name) {
items.remove(name);
}
}
最简单的Bag映像是使用标签,在这之前,假设您如下建立表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);

CREATE TABLE item (
id INT(11) NOT NULL,
name VARCHAR(100) NOT NULL
);
接着定义映射文件,如下所示:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">








column="name"
type="java.lang.String"/>








假设您如下储存对象:
User user1 = new User();
user1.setItems(new ArrayList());
user1.setName("caterpillar");
user1.addItem("Java Gossip");
user1.addItem("Java Gossip");
user1.addItem("Caxxx A80");

User user2 = new User();
user2.setItems(new ArrayList());
user2.setName("momor");
user2.addItem("Snoppy world");

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user1);
session.save(user2);
tx.commit();
session.close();
则数据库中会有如下的数据:
mysql> select * from user;
+----+-------------+
| id | name |
+----+-------------+
| 1 | caterpillar |
| 2 | momor |
+----+-------------+
2 rows in set (0.00 sec)

mysql> select * from item;
+----+-------------------+
| id | name |
+----+-------------------+
| 1 | Java Gossip |
| 1 | Java Gossip |
| 1 | Caxxx A80 |
| 2 | Snoppy world |
+----+-------------------+
4 rows in set (0.00 sec)
您可以如下更新数据:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
User user = (User) session.load(User.class, new Integer(1));
user.removeItem("Java Gossip");
tx.commit();
session.close();
然而注意观察在更新数据时所使用的SQL:
Hibernate: delete from item where id=?
Hibernate: insert into item (id, name) values (?, ?)
Hibernate: insert into item (id, name) values (?, ?)
由于Bag的数据允许重复,当必须更新数据时,无法确定要更新的是哪一笔数据,因而采取的方式是删除集合对象对应的所有数据,然后重新将集合对象中的数据写入数据库,显然的这种作法相当的没有效率。
作为Bag的一种扩充,Hibernate提供idbag,藉由在定义Bag映像时加上"collection-id",让Hibernate可以直接确定所要更新的数据,提高数据库操作的效率,您可以先如下建立表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);

CREATE TABLE item (
cid CHAR(32) NOT NULL,
id INT(11) NOT NULL,
name VARCHAR(100) NOT NULL
);
其中item表格的cid就用于数据更新时定位之用,接着在映像文件中使用标签加以定义:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">








column="name"
type="java.lang.String"/>












使用上面用过的程序片段来储存对象的话,数据库中会有如下的数据:
mysql> select * from user;
+----+-------------+
| id | name |
+----+-------------+
| 1 | caterpillar |
| 2 | momor |
+----+-------------+
2 rows in set (0.00 sec)

mysql> select * from item;
+----------------------------------+------+--------------------+
| cid | id | name |
+----------------------------------+------+--------------------+
| 297eba61056726030105672605df0001 | 1 | Java Gossip |
| 297eba61056726030105672605df0002 | 1 | Java Gossip |
| 297eba61056726030105672605df0003 | 1 | Caxxx A80 |
| 297eba61056726030105672605df0004 | 2 | Snoppy world |
+----------------------------------+------+--------------------+
4 rows in set (0.00 sec)
如果使用上面提到过的程序片段来更新对象的话,则实际上Hibernate会使用以下的SQL来进行更新:
Hibernate: delete from item where cid=?
这一次并不是整个删除集合中的数据,而是直接藉由cid来确定所要更新的数据,比起只使用Bag,idbag的效率好了许多。
内含 Component 的容器
假设您建立了以下的表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);

CREATE TABLE email (
id INT(11) NOT NULL,
address VARCHAR(100) NOT NULL
);
一个user可以有多个email,但不可重复,这可以使用Set来映像,在对应的对象方法,您可以如下设计对象:
package onlyfun.caterpillar;

import java.util.Set;

public class User {
private Integer id;
private Set emails;
....
}
假设您原先预定在Set中储存的是String型态,而后设计时考虑独立设计一个MailAddress类别,而Set中将储存MailAddress的实例,例如:
User.java
package onlyfun.caterpillar;

import java.util.Set;

public class User {
private Integer id;
private String name;
private Set emails;

// 必须要有一个预设的建构方法
// 以使得Hibernate可以使用Constructor.newInstance()建立对象
public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Set getEmails() {
return emails;
}

public void setEmails(Set emails) {
this.emails = emails;
}

public void addEmail(MailAddress mailAddress) {
this.emails.add(mailAddress);
}

public void removeEmail(MailAddress mailAddress) {
this.emails.remove(mailAddress);
}
}
MailAddress.java
package onlyfun.caterpillar;

public class MailAddress {
private String address;

public MailAddress() {
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}

public void sendMail() {
System.out.println("Send mail to " + address);
}
}
在映射文件方面,可以使用来为MailAddress作映射,如下:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

















您可以如下储存对象:
User user = new User();
user.setName("caterpillar");

user.setEmails(new HashSet());
MailAddress mailAddress = new MailAddress();
mailAddress.setAddress("[email protected]");
user.addEmail(mailAddress);

mailAddress = new MailAddress();
mailAddress.setAddress("[email protected]");
user.addEmail(mailAddress);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user);
tx.commit();
session.close();
则数据库中会储存如下的数据:
mysql> select * from user;
+----+-------------+
| id | name |
+----+-------------+
| 1 | caterpillar |
+----+-------------+
1 row in set (0.00 sec)

mysql> select * from email;
+----+-------------------------------------------+
| id | address |
+----+-------------------------------------------+
| 1 | [email protected] |
| 1 | [email protected] |
+----+-------------------------------------------+
2 rows in set (0.00 sec)
在查询时,address表格的数据会封装为MailAddress的实例,一个范例如下:
Session session = sessionFactory.openSession();

User user = (User) session.load(User.class, new Integer(1));
Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
MailAddress mailAddress = (MailAddress) iterator.next();
mailAddress.sendMail();
}
session.close();
容器的排序
从数据库的观点来看,Set、Map、Bag是无序的,而List是有序的,这边所谓的无序或有序,是指将容器中对象储存至数据库时,是否依容器对象中的顺序来储存。
然而从数据库取得数据之后,您也许会希望Set、Map等容器中的对象可以依一定的顺序来排列,您可以从两个层次来容器中的对象排序,一是在加载数据后于JVM中排序,另一是在数据库中直接使用order by子句来排序。
以 Set 这篇文章中的范例来作说明,要在JVM中就数据进行排序,您可以在映像文件中使用sort属性来定义容器的排序,这适用于Set与Map,例如:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">




....


column="address"/>




sort="natural"表示使用对象的comparaTo()方法来进行排序,容器中的对象上必须有实作java.lang.Comparable 接口,例如String就有实作java.lang.Comparable接口,结果会使用字典顺序来排列容器中的对象。
您可以实现自己的排序方式,只要定义一个类别来实作java.util.Comparator接口,例如:
CustomComparator.java
package onlyfun.caterpillar;

import java.util.Comparator;

public class CustomComparator implements Comparator {
public int compare(Object o1, Object o2) {
if (((String) o1).equals(o2))
return 0;
return ((Comparable) o1).compareTo(o2) * -1;
}
}
在自订的Comparator中,如果两个对象的顺序相同会传回0,而为了方便比较对象,要求传入的对象必须实作Comparable接口(例如 String对象就有实作Comparable接口),范例中只是简单的将原来compareTo()传回的值乘以负一,如此就可以简单的让排列顺序相反,接着可以在映射文件中指定自订的Comparator类别:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">




....
sort="onlyfun.caterpillar.CustomComparator">

column="address"/>




Bag与List并不适用于这种方式。
另一个排序的方式则是在数据库中进行,直接使用order by子句来排序,这可以在映像文件中使用order-by属性来指定,例如:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">




....


column="address"/>




观察Hibernate所使用的SQL可以看到order by子句:
Hibernate:
select emails0_.id as id0_, emails0_.address as address0_
from email emails0_ where emails0_.id=? order by emails0_.address desc
容器的延迟初始(Lazy Initialization)
有时候您只是想要获得对象中某个属性的数据,如果您的对象中包括Set等容器对象,若从数据库中加载数据时全部加载所有的对象,却只是为了取得某个属性,显然的这样很没有效率。
以Set中的范例来说,如果您只是想取得对象之后,显示对象的某些属性,例如name属性:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());
session.close();
在这个例子中,email的信息不必要从数据库中全部加载,在Hibernate中支持容器的延迟初始(Lazy onitialization),只有在真正需要容器对象中的数据时,才从数据库中取得数据,预设容器类会使用延迟加载的功能,例如上面的程序实际上会使用以下的SQL:
Hibernate: select user0_.id as id0_, user0_.name as name0_0_ from user user0_ where user0_.id=?
可以藉由映像文件中的lazy属性来设定是否使用延迟初始,例如在映射文件中如下设定:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">




....


column="address"/>




由于lazy属性被设定为false,延迟初始的功能被关闭,所以上面的程序会使用以下的SQL来查询:
Hibernate:
select user0_.id as id0_, user0_.name as name0_0_
from user user0_ where user0_.id=?
Hibernate:
select emails0_.id as id0_, emails0_.address as address0_
from email emails0_ where emails0_.id=?
所有的容器对象之数据一并被查询了,即使程序中还不会使用到容器中的对象信息。
在启用延迟初始的情况下,如果如下查询数据:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());
Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
session.close();
在开启SQL显示的情况下,会显示以下的内容:
Hibernate:
select user0_.id as id0_, user0_.name as name0_0_
from user user0_ where user0_.id=?

caterpillar

Hibernate:
select emails0_.id as id0_, emails0_.address as address0_
from email emails0_ where emails0_.id=?

[email protected]
[email protected]
可以看到,只有在需要查询容器中对象时,才会向数据库索取数据。
使用延迟初始时,由于在需要数据时会向数据库进行查询,所以session不能关闭,如果关闭会丢出LazyInitializationException 例外,例如下面的程序就会丢出例外:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());

session.close();

Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
如果您使用了延迟初始,而在某些时候仍有需要在session关闭之后取得相关对象,则可以使用Hibernate.initialize()来先行加载相关对象,例如:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());

Hibernate.initialize(user.getEmails()); // 先加载容器中的对象

session.close();

Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
即使启用延迟初始,在Hibernate.initialize()该行,email容器中的对象已经被加载,所以即使关闭session也无所谓了,这种情况发生在某个情况下,您启用了延迟初始,而使用者操作过程中,会在稍后一阵子才用到email容器,您不能浪费session在这段时间的等待上,所以您先行加载容器对象,直接关闭session以节省资源。
关系映射
多对一
一个实体简单的说就是在数据库中拥有一个表格,并拥有自已的数据库识别(Database identity)。
一个简单的实体与实体间之关系为多对一的关系,例如在学校宿舍中,使用者与房间的关系就是多对一的关系,多个使用者可以居住于一个房间。

如上图所示的,可以藉由room_id让使用者与房间产生关联,您可以如下建立user与room表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
room_id INT(11)
);

CREATE TABLE room (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
address VARCHAR(100) NOT NULL default ''
);
用程序来表示的话,首先看看User类别:
User.java
package onlyfun.caterpillar;

public class User {
private Integer id;
private String name;
private Room room;

public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer 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;
}
}
User类别中有一room属性,将参考至Room实例,多个User实例可共同参考一个Room实例,Room类别设计如下:
Room.java
package onlyfun.caterpillar;

public class Room {
private Integer id;
private String address;

public Room() {
}

public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}
}
在映射文件方面,先来看看Room.hbm.xml:
Room.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">








column="address"
type="java.lang.String"/>



没什么,很简单的一个映射文件,而在User.hbm.xml中,使用标签来映像多对一关系:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">










column="room_id"
class="onlyfun.caterpillar.Room"
cascade="all"
outer-join="true"/>



的设定中,cascade表示主控方(User)进行save-update、delete等相关操作时,被控方(Room)是否也一并进行相关操作,简单的说,也就是您储存或更新User实例时,当中的Room实例是否一并对数据库发生储存或操作,设定为 all,表示主控方任何操作,被控方也进行对应操作。
一个储存的例子如下:
Room room1 = new Room();
room1.setAddress("NTU-M8-419");
Room room2 = new Room();
room2.setAddress("NTU-G3-302");

User user1 = new User();
user1.setName("bush");
user1.setRoom(room1);

User user2 = new User();
user2.setName("caterpillar");
user2.setRoom(room1);

User user3 = new User();
user3.setName("momor");
user3.setRoom(room2);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

session.save(user1); // 主控方操作,被控方也会对应操作
session.save(user2);
session.save(user3);

tx.commit();
session.close();
数据库中将储存以下的内容:
mysql> select * from user;
+-------+-------------+-----------+
| id | name | room_id |
+-------+-------------+-----------+
| 1 | bush | 1 |
| 2 | caterpillar | 1 |
| 3 | momor | 2 |
+-------+-------------+-----------+
3 rows in set (0.00 sec)

mysql> select * from room;
+----+-------------------+
| id | address |
+----+-------------------+
| 1 | NTU-M8-419 |
| 2 | NTU-G3-302 |
+----+-------------------+
2 rows in set (0.00 sec)
在查询时的例子如下:
Session session = sessionFactory.openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());
System.out.println(user.getRoom().getAddress());
session.close();
在设定outer-join为true的情况下,Hibernate将使用以下的SQL一次查询所有的数据:
Hibernate:
select user0_.id as id1_, user0_.name as name0_1_, user0_.room_id as room3_0_1_,
room1_.id as id0_, room1_.address as address1_0_ from user user0_
left outer join room room1_ on user0_.room_id=room1_.id where user0_.id=?
在不设定outer-join为true的情况下,Hibernate则使用以下的SQL分别查询user与room表格:
Hibernate:
select user0_.id as id0_, user0_.name as name0_0_, user0_.room_id as room3_0_0_
from user user0_ where user0_.id=?
Hibernate:
select room0_.id as id0_, room0_.address as address1_0_
from room room0_ where room0_.id=?
cascade 的意义
在Java程序中,对象与对象之间会透过某些关系互相参考,如果有一个对象已经是持久化对象,被它参考的对象直觉上也应该要持久化,以维持对象之间关联的完整性,这是藉由可达性完成持久化(Persistence by reachability)的基本概念。
如果将对象之间的关联想象为一个树形图,从某一个持久化物件为树根出发,父节点若是持久化对象,则被父节点参考到的子节点应自动持久化,而另一方面,如果有一子节点没办法藉由任何的父节点来参考至它,则它没有被持久化的需求,它应从数据库中加以删除。
Hibernate并没有完全实现以上的概念,它让使用者自行决定自动持久化的方式,当对象之间被指定关联(例如多对一、一对多等),您可以决定被持久化对象关联的暂存对象是否进行自动持久化与如何自动持久化。
在Hibernate中是以映像文件中卷标上的cascade属性来设定,预设上是none,以多对一中的范例来说,如果不设定cascade为true,则您必须分别对User实例与Room实例进行储存:
Room room1 = new Room();
room1.setAddress("NTU-M8-419");
Room room2 = new Room();
room2.setAddress("NTU-G3-302");

User user1 = new User();
user1.setName("bush");
user1.setRoom(room1);

User user2 = new User();
user2.setName("caterpillar");
user2.setRoom(room1);

User user3 = new User();
user3.setName("momor");
user3.setRoom(room2);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

// 储存Room实例
session.save(room1);
session.save(room2);
// 储存User实例
session.save(user1);
session.save(user2);
session.save(user3);

tx.commit();
session.close();
使用cascade自动持久化时,会先检查被关联对象的id属性,未被持久化的对象是否储存是藉由id属性的unsaved-value决定,预设是 null,也就是没有参考至任何值时储存对象,如果您使用int、long这样的原生型态(Primitive type)时,由于数据成员的初始会被设定为0,所以您必须自行指定默认值,例如(如果id的数据型态是long的话):



一对多
在多对一中,User对Room是多对一的关系,User实例维护着对Room实例的参考,如果将这个关系反过来,由Room实例维护对多个User实例的数据,就是一对多的关系。
具体来说,可以设计User类别如下:
User.java
package onlyfun.caterpillar;

public class User {
private Integer id;
private String name;

public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
而在Room类别中,使用Set来记录多个User:
Room.java
package onlyfun.caterpillar;

import java.util.Set;

public class Room {
private Integer id;
private String address;
private Set users;

public Room() {
}

public Integer getId() {
return id;
}
public void setId(Integer 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;
}

public void addUser(User user) {
users.add(user);
}

public void removeUser(User user) {
users.remove(user);
}
}
这种方式即所谓单向一对多关系,也就是Room实例知道User实例的存在,而User实例则没有意识到Room实例。
(在多对一中,则是单向多对一关系,即User知道Room的存在,但Room不知道User的存在。)
在映射文件上,先来看看User.hbm.xml:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">













在单向关系中,被参考的对象其映像文件就如单一实体一样的配置,接下来看看Room.hbm.xml,使用卷标配置一对多:
Room.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">






column="address"
type="java.lang.String"/>







接着您可以如下储存对象:
User user1 = new User();
user1.setName("bush");
User user2 = new User();
user2.setName("caterpillar");
User user3 = new User();
user3.setName("momor");

Room room1 = new Room();
room1.setUsers(new HashSet());
room1.setAddress("NTU-M8-419");
room1.addUser(user1);
room1.addUser(user2);

Room room2 = new Room();
room2.setUsers(new HashSet());
room2.setAddress("NTU-G3-302");
room2.addUser(user3);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

session.save(room1); // cascade 操作
session.save(room2);

tx.commit();
session.close();
数据库中将储存以下的表格:
mysql> select * from user;
| id | name | room_id |
+----+-------------+------------+
| 1 | bush | 1 |
| 2 | caterpillar | 1 |
| 3 | momor | 2 |
+----+-------------+------------+
3 rows in set (0.01 sec)

mysql> select * from room;
+----+------------------+
| id | address |
+----+------------------+
| 1 | NTU-M8-419 |
| 2 | NTU-G3-302 |
+----+------------------+
2 rows in set (0.00 sec)
双向关联(inverse 的意义)
在多对一、一对多中都是单向关联,也就是其中一方关联到另一方,而另一方不知道自己被关联。
如果让双方都意识到另一方的存在,这就形成了双向关联,在多对一、一对多的例子可以改写一下,重新设计User类别如下:
User.java
package onlyfun.caterpillar;

public class User {
private Integer id;
private String name;
private Room room;

public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer 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类别如下:
Room.java
package onlyfun.caterpillar;

import java.util.Set;

public class Room {
private Integer id;
private String address;
private Set users;

public Room() {
}

public Integer getId() {
return id;
}
public void setId(Integer 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;
}

public void addUser(User user) {
users.add(user);
}

public void removeUser(User user) {
users.remove(user);
}
}
如此,User实例可参考至Room实例而维持多对一关系,而Room实例记得User实例而维持一对多关系。
在映射文件方面,可以如下撰写:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">







column="room_id"
class="onlyfun.caterpillar.Room"
cascade="save-update"
outer-join="true"/>


Room.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">








column="address"
type="java.lang.String"/>








映像文件双方都设定了cascade为save-update,所以您可以用多对一的方式来维持关联:
User user1 = new User();
user1.setName("bush");

User user2 = new User();
user2.setName("caterpillar");

Room room1 = new Room();
room1.setAddress("NTU-M8-419");

user1.setRoom(room1);
user2.setRoom(room1);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

session.save(user1);
session.save(user2);

tx.commit();
session.close();
或是反过来由一对多的方式来维持关联:
User user1 = new User();
user1.setName("bush");

User user2 = new User();
user2.setName("caterpillar");

Room room1 = new Room();
room1.setUsers(new HashSet());
room1.setAddress("NTU-M8-419");
room1.addUser(user1);
room1.addUser(user2);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

session.save(room1);

tx.commit();
session.close();
这边有个效率议题可以探讨,上面的程序片段Hibernate将使用以下的SQL进行储存:
Hibernate: insert into room (address) values (?)
Hibernate: insert into user (name, room_id) values (?, ?)
Hibernate: insert into user (name, room_id) values (?, ?)
Hibernate: update user set room_id=? where id=?
Hibernate: update user set room_id=? where id=?
上面的程序写法表示关联由Room单方面维持,而主控方也是Room,User不知道Room的room_id是多少,所以必须分别储存Room与User之后,再更新user的room_id。
在一对多、多对一形成双向关联的情况下,可以将关联维持的控制权交给多的一方,这样会比较有效率,理由不难理解,就像是在公司中,老板要记住多个员工的姓名快,还是每一个员工都记得老板的姓名快。
所以在一对多、多对一形成双向关联的情况下,可以在「一」的一方设定控制权反转,也就是当储存「一」的一方时,将关联维持的控制权交给「多」的一方,以上面的例子来说,可以设定Room.hbm.xml如下:
Room.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">








column="address"
type="java.lang.String"/>








由于关联的控制权交给「多」的一方了,所以直接储存「一」方前,「多」的一方必须意识到「一」的存在,所以程序片段必须改为如下:
User user1 = new User();
user1.setName("bush");

User user2 = new User();
user2.setName("caterpillar");

Room room1 = new Room();
room1.setUsers(new HashSet());
room1.setAddress("NTU-M8-419");
room1.addUser(user1);
room1.addUser(user2);

// 多方必须意识到单方的存在
user1.setRoom(room1);
user2.setRoom(room1);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

session.save(room1);

tx.commit();
session.close();
上面的程序片段Hibernate将使用以下的SQL:
Hibernate: insert into room (address) values (?)
Hibernate: insert into user (name, room_id) values (?, ?)
Hibernate: insert into user (name, room_id) values (?, ?)
如果控制权交给另一方了,而另一方没有意识到对方的存在的话会如何?试着将上面的程序片段中user1.setRoom(room1);与user2.setRoom(room1);移去,执行之后,您会发现数据库中room_id会出现null值,这种结果就好比在多对一中,您没有分配给User一个Room,理所当然的,room_id会出现null。
一对一(唯一外键关联
现在考虑每一个User配给一间Room,形成一对一,user表格透过room_id作为外键参考至room:

在表格建立方面,使用多对一中的表格建立语句就可以了:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default '',
room_id INT(11)
);

CREATE TABLE room (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
address VARCHAR(100) NOT NULL default ''
);
对象方面,User的实例会参考至Room实例,而Room实例也参考至User实例:
User.java
package onlyfun.caterpillar;

public class User {
private Integer id;
private String name;
private Room room;

public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer 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 Integer id;
private String address;
private User user;

public Room() {
}

public Integer getId() {
return id;
}
public void setId(Integer 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;
}
}
使用外键来完成一对一,其实就是限制多对一关系中,「多」的一方只能有一个参考至「一」的一方,也就是多对一关系的一个特例,这可以在映像文件中使用标签时,加上"unique"属性来设定,例如:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">










column="room_id"
class="onlyfun.caterpillar.Room"
cascade="all"
outer-join="true"
unique="true"/>



到这边为止,单向一对一的映像已经完成,如果要再完成双向一对一的关系,则可以在Room.hbm.xml中使用标签来定义:
Room.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">








column="address"
type="java.lang.String"/>

class="onlyfun.caterpillar.User"
property-ref="room"/>



中,property-ref告诉Hibernate,查询出user并将其参考至room。
一个储存的例子如下:
User user1 = new User();
user1.setName("bush");
Room room1 = new Room();
room1.setAddress("NTU-M8-419");
user1.setRoom(room1);

User user2 = new User();
user2.setName("caterpillar");
Room room2 = new Room();
room2.setAddress("NTU-M8-418");
user2.setRoom(room2);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

session.save(user1);
session.save(user2);

tx.commit();
session.close();
在查询Room时,User也会一加载,例如:
Session session = sessionFactory.openSession();
Room room = (Room) session.load(Room.class, new Integer(23));
System.out.println(room.getUser().getName());
session.close();
上面的查询程序,Hibernate将使用以下的SQL:
Hibernate:
select
room0_.id as id1_, room0_.address as address1_1_, user1_.id as id0_, user1_.name as name0_0_,
user1_.room_id as room3_0_0_
from room room0_ left outer join user user1_ on room0_.id=user1_.room_id where room0_.id=?
一对一(主键关联)
一对一关联的另一种方式,是限制两个实体的主键必须一致,如此直接透过两个表格的主键就可确定一对一关联,而不用额外的外键参考。

例如user与room表格,可以如下建立:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);

CREATE TABLE room (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
address VARCHAR(100) NOT NULL default ''
);
User类别与Room类别的设计使用一对一(唯一外键关联)中的设计即可,接着在User.hbm.xml方面如下设计:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">










class="onlyfun.caterpillar.Room"
cascade="all"/>



在Room.hbm.xml的设计方面如下:
Room.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">






user



column="address"
type="java.lang.String"/>

class="onlyfun.caterpillar.User"
constrained="true"/>



在Room的id主键上,使用foreign表示与外键共享主键,也就是与User实体共享主键,而constrained设定为true,表示约束room的主键必须与user中对应数据的主键相同。
一个储存的实例如下:
User user1 = new User();
user1.setName("bush");
Room room1 = new Room();
room1.setAddress("NTU-M8-419");

// 互相设定关联
user1.setRoom(room1);
room1.setUser(user1);

User user2 = new User();
user2.setName("caterpillar");
Room room2 = new Room();
room2.setAddress("NTU-M8-418");

// 互相设定关联
user2.setRoom(room2);
room2.setUser(user2);

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

session.save(user1);
session.save(user2);

tx.commit();
session.close();
数据库中将有以下的储存结果:
mysql> select * from user;
+----+-------------+
| id | name |
+----+-------------+
| 1 | bush |
| 2 | caterpillar |
+----+-------------+
2 rows in set (0.00 sec)

mysql> select * from room;
+----+------------------+
| id | address |
+----+------------------+
| 1 | NTU-M8-419 |
| 2 | NTU-M8-418 |
+----+------------------+
2 rows in set (0.00 sec)
多对多
在数据库表格上要进行多对多对应,可以藉由一个中介表格来完成,也就是藉由多对一、一对多来完成多对多关联。

多对多由于使用了中介表格,在查询效率不彰,且在程序的对象模式上,多对多会使得对象与对象之间彼此依赖,并不是一个很好的设计方式,在设计上应避免使用多对多关系。
如果一定要使用多对多关系的话,在表格上先如下建立:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
name VARCHAR(100) NOT NULL default ''
);

CREATE TABLE user_server (
user_id INT(11),
server_id INT(11)
);

CREATE TABLE server (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
address VARCHAR(100) NOT NULL default ''
);
先设计User类别如下:
User.java
package onlyfun.caterpillar;

import java.util.Set;

public class User {
private Integer id;
private String name;
private Set servers;

public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer 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类别如下:
Server.java
package onlyfun.caterpillar;

import java.util.Set;

public class Server {
private Integer id;
private String address;
private Set users;

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public Set getUsers() {
return users;
}

public void setUsers(Set users) {
this.users = users;
}
}
在映像文件上,使用标签来完成映像关系:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">










table="user_server"
cascade="save-update">


column="server_id"/>




注意到cascade是设定为save-update,因为在多对多的关系中,很少因为删除其中之一,而所关联的实体都要一并删除的,所以设定save-update,表示在save或update时,一并对关联的对象进行对应的save或update。
Server.hbm.xml的定义如下:
Server.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">







table="user_server"
inverse="true"
cascade="save-update">


column="user_id"/>




一个储存时的例子如下:
Server server1 = new Server();
server1.setAddress("PC-219");
server1.setUsers(new HashSet());

Server server2 = new Server();
server2.setAddress("PC-220");
server2.setUsers(new HashSet());

Server server3 = new Server();
server3.setAddress("PC-221");
server3.setUsers(new HashSet());

User user1 = new User();
user1.setName("caterpillar");
user1.setServers(new HashSet());

User user2 = new User();
user2.setName("momor");
user2.setServers(new HashSet());

// 多对多,互相参考
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();
执行后数据库的内容如下:
mysql> select * from user;
+----+--------------+
| id | name |
+----+--------------+
| 1 | caterpillar |
| 2 | momor |
+----+--------------+
2 rows in set (0.00 sec)

mysql> select * from user_serv
+----------+-------------+
| user_id | server_id |
+----------+-------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
| 2 | 2 |
+----------+-------------+
5 rows in set (0.00 sec)

mysql> select * from server;
+----+-----------+
| id | address |
+----+-----------+
| 1 | PC-219 |
| 2 | PC-221 |
| 3 | PC-220 |
+----+-----------+
3 rows in set (0.00 sec)
快取
二级快取(Second-level)
Hibernate的[Session level 快取]随着Session生命周期起始与消灭。
以第一个 Hibernate中的范例来说,在未使用二级快取的情况下,如果使用以下的程序片段来查询数据:
Session session = sessionFactory.openSession();
User user1 = (User) session.load(User.class, new Integer(1));
user1.getName();
session.close();

session = sessionFactory.openSession();
User user2 = (User) session.load(User.class, new Integer(1));
user2.getName();
session.close();
则Hibernate将会使用以下的SQL来进行数据查询:
Hibernate:
select user0_.id as id0_, user0_.name as name0_0_, user0_.age as age0_0_
from user user0_ where user0_.id=?
Hibernate:
select user0_.id as id0_, user0_.name as name0_0_, user0_.age as age0_0_
from user user0_ where user0_.id=?
由于Session被关闭,Session level无法起作用,所以第二次的查询仍必须向数据库直接查询。
Hibernate二级快取可以跨越数个Session,二级快取由同一个SessionFactory所建立的Session所共享,因而又称为SessionFactory level快取。
Hibernate本身并未提供二级快取的实现,而是藉由第三方(Third-party)产品来实现,Hibernate预设使用EHCache作为其二级快取的实现,在最简单的情况下,您只需在Hibernate下撰写一个ehcache.xml作为EHCache的资源定义档,可以在 Hibernate下载档案中的etc目录下找到一个已经撰写好的ehcache.xml,以下撰写最简单的ehcache.xml:
ehcache.xml


maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>

将这个档案放在Hibernate项目Classpath可存取到的路径下,接着重新运行上面的程序片段,您可以发现Hibernate将使用以下的SQL进行查询:
Hibernate:
select user0_.id as id0_, user0_.name as name0_0_, user0_.age as age0_0_
from user user0_ where user0_.id=?
二级快取被同一个SessionFactory所建立的Session实例所共享,所以即使关闭了Session,下一个Session仍可使用二级快取,在查询时,Session会先在Session level快取中查询看有无数据,如果没有就试着从二级快取中查询数据,查到数据的话就直接返回该笔数据,所以在上例中,第二次无需再向数据库进行SQL查询。
如果打算清除二级快取的资料,可以使用SessionFactory的evict()方法,例如:
sessionFactory.evict(User.class, user.getId());
如果打算在Hibernate中使用其它第三方产品进行快取,则可以在hibernate.cfg.xml中定义hibernate.cache.provider_class属性,例如:
hibernate.cfg.xml

"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">





....

org.hibernate.cache.HashtableCacheProvider

....




HashtableCache是Hibernate自己所提供的二级快取实现,不过性能与功能上有限,只用于开发时期的测试之用。
可以在映射文件中指定快取策略,使用卷标在映像实体或Collection上设定快取策略,例如:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

















可以设定的策略包括read-only、read-write、nonstrict-read-write与transactional,并不是每一个第三方快取实现都支持所有的选项,每一个选项的使用时机与支持的产品,可以直接参考Hibernate官方参考快册的 20.2.The Second Level Cache 。
Query 快取
Hibernate的Session level 快取会在使用Session的load()方法时起作用,在设定条件进行查询时,无法使用快取的功能,现在考虑一种情况,您的数据库表格中的数据很少变动,在使用Query查询数据时,如果表格内容没有变动,您希望能重用上一次查询的结果,除非表格内容有变动才向数据库查询。
您可以开启Query的快取功能,因为要使用Query的快取功能必须在两次查询时所使用的SQL相同,且两次查询之间表格没有任何数据变动下才有意义, 所以Hibernate预设是关闭这个功能的,如果您觉得符合这两个条件,那么可以试着开启Query快取功能来看看效能上有无改进。
先来看看下面的查询程序片段:
Session session = sessionFactory.openSession();

String hql = "from User";

Query query = session.createQuery(hql);
List users = query.list();

for(int i = 0; i < users.size(); i++) {
User user = (User) users.get(i);
System.out.println(user.getName());
}

query = session.createQuery(hql);
users = query.list();

for(int i = 0; i < users.size(); i++) {
User user = (User) users.get(i);
System.out.println(user.getName());
}

session.close();
在不启用Query快取的情况下,Hibernate会使用两次SQL向数据库查询数据:
Hibernate:
select user0_.id as id, user0_.name as name0_, user0_.age as age0_
from user user0_
momor
caterpillar

Hibernate: select user0_.id as id, user0_.name as name0_, user0_.age as age0_ from user user0_
momor
caterpillar
如果打算启用Query快取功能,首先在hibernate.cfg.xml中设定hibernate.cache.use_query_cache属性:
hibernate.cfg.xml

"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">




....

true
....




然后在每次建立Query实例时,执行setCacheable(true):
Session session = sessionFactory.openSession();

String hql = "from User";

Query query = session.createQuery(hql);
// 使用Query快取
query.setCacheable(true);
List users = query.list();

for(int i = 0; i < users.size(); i++) {
User user = (User) users.get(i);
System.out.println(user.getName());
}

query = session.createQuery(hql);
// 使用Query快取
query.setCacheable(true);
users = query.list();

for(int i = 0; i < users.size(); i++) {
User user = (User) users.get(i);
System.out.println(user.getName());
}

session.close();
Hibernate在启用Query快取后,会保留执行过的查询SQL与查询结果,在下一次查询时会看看SQL是否相同,并看看对应的数据库表格是否有变动(Update/Delete/Insert),如果SQL相同且数据库也没有变动,则将Query快取中的查询结果返回,上面的程序片段将使用一次 SQL查询,第二次查询时直接返回快取中的结果:
Hibernate:
select user0_.id as id, user0_.name as name0_, user0_.age as age0_
from user user0_
momor
caterpillar
momor
caterpillar
Query.list、iterator
Query上有list()与iterator()方法,两者的差别在于list()方法在读取数据时,并不会利用到快取,而是直接再向数据库查询,而iterator()则将读取到的数据写到快取,并于读取时再次利用。
来看看下面的程序:
Session session = sessionFactory.openSession();

Query query = session.createQuery("from User");
List users = query.list();
users = query.list();

session.close();
这个程序片段会使用两次SQL来查询数据库:
Hibernate:
select user0_.id as id, user0_.name as name0_, user0_.age as age0_
from user user0_

Hibernate: select user0_.id as id, user0_.name as name0_, user0_.age as age0_
from user user0_
如果在Session关闭之前,要再将所有数据在取出,可以使用iterator()方法,例如:
Session session = sessionFactory.openSession();

Query query = session.createQuery("from User");
Iterator users = query.iterate();
users = query.iterate();

session.close();
这个程序片段会使用一次SQL向数据库查询,第二次则直接从快取中取得数据:
Hibernate: select user0_.id as col_0_0_ from user user0_
由于使用iterator()方法时会使用到Session level快取,所以在查询大量数据时,会耗用大量的内存,必要时可以使用Session的evict()或clear()方法来清除快取。
Locking
悲观锁定(Pessimistic Locking)
在多个客户端可能读取同一笔数据或同时更新一笔数据的情况下,必须要有访问控制的手段,防止同一个数据被修改而造成混乱,最简单的手段就是对数据进行锁定,在自己进行数据读取或更新等动作时,锁定其它客户端不能对同一笔数据进行任何的动作。
悲观锁定(Pessimistic Locking)一如其名称所示,悲观的认定每次资料存取时,其它的客户端也会存取同一笔数据,因此对该笔数据进行锁定,直到自己操作完成后解除锁定。
悲观锁定通常透过系统或数据库本身的功能来实现,依赖系统或数据库本身提供的锁定机制,Hibernate即是如此,可以利用Query或Criteria的setLockMode()方法来设定要锁定的表或列(Row)及其锁定模式,可设定的锁定模式有以下的几个:
LockMode.UPGRADE:利用数据库的for update子句进行锁定。
LockMode.UPGRADE_NOWAIT:使用for update nowait子句进行锁定,在Oracle数据库中使用。
一个设定锁定的例子如下:
Session session = sessionFactory.openSession();
Query query = session.createQuery("from User user");
query.setLockMode("user", LockMode.UPGRADE);
List users = query.list();
...
session.close();
这个程序片段会使用以下的SQL进行查询:
Hibernate: select user0_.id as id, user0_.name as name0_, user0_.age as age0_
from user user0_ for update
也可以在使用Session的load()或是lock()时指定锁定模式以进行锁定。
另外还有三种加锁模式Hibernate内部自动对数据进行锁定,与数据库无关:
LockMode.WRITE:在insert或update时进行锁定,Hibernate会在save()方法时自动获得锁定。
LockMode.READ:在读取记录时Hibernate会自动获得锁定。
LockMode.NONE:没有锁定。
如果数据库不支持所指定的锁定模式,Hibernate会选择一个合适的锁定替换,而不是丢出一个例外。
乐观锁定(Optimistic Locking)
悲观锁定假定任何时刻存取数据时,都可能有另一个客户也正在存取同一笔数据,因而对数据采取了数据库层次的锁定状态,在锁定的时间内其它的客户不能对数据 进行存取,对于单机或小系统而言,这并不成问题,然而如果是在网络上的系统,同时间会有许多联机,如果每一次读取数据都造成锁定,其后继的存取就必须等 待,这将造成效能上的问题,造成后继使用者的长时间等待。
乐观锁定(Optimistic locking)则乐观的认为资料的存取很少发生同时存取的问题,因而不作数据库层次上的锁定,为了维护正确的数据,乐观锁定使用应用程序上的逻辑实现版本控制的解决。
在不实行悲观锁定策略的情况下,数据不一致的情况一但发生,有几个解决的方法,一种是先更新为主,一种是后更新的为主,比较复杂的就是检查发生变动的数据来实现,或是检查所有属性来实现乐观锁定。
Hibernate中透过版本号检查来实现后更新为主,这也是Hibernate所推荐的方式,在数据库中加入一个version字段记录,在读取数据时 连同版本号一同读取,并在更新数据时比对版本号与数据库中的版本号,如果等于数据库中的版本号则予以更新,并递增版本号,如果小于数据库中的版本号就丢出 例外。
实际来透过范例了解Hibernate的乐观锁定如何实现,首先在数据库中新增一个表格:
CREATE TABLE user (
id INT(11) NOT NULL auto_increment PRIMARY KEY,
version INT,
name VARCHAR(100) NOT NULL default '',
age INT
);
这个user表格中的version用来记录版本号,以供Hibernate实现乐观锁定,接着设计User类别,当中必须包括version属性:
User.java
package onlyfun.caterpillar;

public class User {
private Integer id;
private Integer version; // 增加版本属性
private String name;
private Integer age;

public User() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public Integer getVersion() {
return version;
}

public void setVersion(Integer version) {
this.version = version;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}
在映射文件的定义方面,则如下所示:
User.hbm.xml

PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">



table="user"
optimistic-lock="version">





column="version"
type="java.lang.Integer"/>








注意卷标必须出现在卷标之后,接着您可以试着在数据库中新增数据,例如:
User user = new User();
user.setName("caterpillar");
user.setAge(new Integer(30));
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user);
tx.commit();
session.close();
您可以检视数据库中的数据,每一次对同一笔数据进行更新,version字段的内容都会自动更新,接着来作个实验,直接以范例说明:
// 有使用1者开启了一个session1
Session session1 = sessionFactory.openSession();
// 在这之后,马上有另一个使用者2开启了session2
Session session2 = sessionFactory.openSession();

Integer id = new Integer(1);

// 使用者1查询数据
User userV1 = (User) session1.load(User.class, id);
// 使用者2查询同一笔数据
User userV2 = (User) session2.load(User.class, id);

// 此时两个版本号是相同的
System.out.println(" v1 v2 "
+ userV1.getVersion().intValue() + " "
+ userV2.getVersion().intValue());

Transaction tx1 = session1.beginTransaction();
Transaction tx2 = session2.beginTransaction();

// 使用者1更新数据
userV1.setAge(new Integer(31));
tx1.commit();

// 此时由于数据更新,数据库中的版本号递增了
// 两笔数据版本号不一样了
System.out.println(" v1 v2 "
+ userV1.getVersion().intValue() + " "
+ userV2.getVersion().intValue());

// userV2 的 age 资料还是旧的
// 数据更新
userV2.setName("justin");
// 因版本号比数据库中的旧
// 送出更新数据会失败,丢出StableObjectStateException例外
tx2.commit();

session1.close();
session2.close();
运行以下的程序片段,会出现以下的结果:
Hibernate:
select user0_.id as id0_, user0_.version as version0_0_, user0_.name as name0_0_, user0_.age as age0_0_
from user user0_ where user0_.id=?

Hibernate:
select user0_.id as id0_, user0_.version as version0_0_, user0_.name as name0_0_, user0_.age as age0_0_
from user user0_ where user0_.id=?
v1 v2 0 0

Hibernate:
update user set version=?, name=?, age=? where id=? and version=?
v1 v2 1 0

Hibernate:
update user set version=?, name=?, age=? where id=? and version=?
16:11:43,187 ERROR AbstractFlushingEventListener:277 - Could not synchronize database state with session
org.hibernate.StaleObjectStateException:
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [onlyfun.caterpillar.User#1]
at org.hibernate.persister.entity.BasicEntityPersister.check(BasicEntityPersister.java:1441)
由于新的版本号是1,而userV2的版本号还是0,因此更新失败丢出StableObjectStateException,您可以捕捉这个例外作善后处理,例如在处理中重新读取数据库中的数据,同时将目前的数据与数据库中的数据秀出来,让使用者有机会比对不一致的数据,以决定要变更的部份,或者您可以 设计程序自动读取新的数据,并比对真正要更新的数据,这一切可以在背景执行,而不用让您的使用者知道。
要注意的是,由于乐观锁定是使用系统中的程序来控制,而不是使用数据库中的锁定机制,因而如果有人特意自行更新版本讯息来越过检查,则锁定机制就会无效, 例如在上例中自行更改userV2的version属性,使之与数据库中的版本号相同的话就不会有错误,像这样版本号被更改,或是由于数据是由外部系统而来,因而版本信息不受控制时,锁定机制将会有问题,设计时必须注意。
Validatable、Lifecycle、Interceptor
Lifecycle 接口、Validatable 接口
可以在实体对象定义时实作Lifecycle接口,这个接口定义如下:
Lifecycle.java
package org.hibernate.classic;

import java.io.Serializable;
import org.hibernate.CallbackException;
import org.hibernate.Session;

public interface Lifecycle {
public static final boolean VETO = true;
public static final boolean NO_VETO = false;

public boolean onSave(Session s) throws CallbackException;
public boolean onUpdate(Session s) throws CallbackException;
public boolean onDelete(Session s) throws CallbackException;
public void onLoad(Session s, Serializable id);
}
当对象实作Lifecycle接口时,会在save()、update()、delete()、load()等方法执行之前呼叫对应的onSave()、 onUpdate()、onDelete()与onLoad(),其中onSave()、onUpdate()、onDelete()与onLoad() 若传回true或丢出CallbackException,则对应的操作中止。
可以在实体对象定义时实作Validatable接口,其定义如下:
Validatable.java
package org.hibernate.classic;

public interface Validatable {
public void validate() throws ValidationFailure;
}
如果定义时实作了Validatable接口,当对象被持久化之前会呼叫validate()方法,如果丢出ValidationFailure,则验证失败,对象的数据不会储存至数据库中。
Interceptor 界面
您可以在开启Session时加载一个自订Interceptor,这个Interceptor会在对应的动作发生之前呼叫对应的方法,方法是让您定义的Interceptor实作Interceptor接口,接口的定义如下:
Interceptor.java
package org.hibernate;

import java.io.Serializable;
import java.util.Iterator;

import org.hibernate.type.Type;

public interface Interceptor {
// 加载对象之前执行
public
boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types)
throws CallbackException;

// flush 时,如果发现有Dirty data,则执行此方法
public
boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState,
String[] propertyNames, Type[] types) throws CallbackException;

// 储存对象前执行
public
boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types)
throws CallbackException;

// 删除对象前执行
public
void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types)
throws CallbackException;

// 在 flush 前执行
public void preFlush(Iterator entities) throws CallbackException;

// 在 flush 后执行
public void postFlush(Iterator entities) throws CallbackException;

// 判断传入的对象是否为 transient 状态
public Boolean isTransient(Object entity);

// flush 前呼叫这个方法判断 Dirty data
// 传回Dirty data属性索引或null采预设行为
public
int[] findDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState,
String[] propertyNames, Type[] types);

// 手动建立实体对象,如果传回 null,则使用预设的建构方法建立实例
public Object instantiate(String entityName, EntityMode entityMode, Serializable id)
throws CallbackException;

// 传回实体名称
public String getEntityName(Object object) throws CallbackException;

// 取得实体对象
public Object getEntity(String entityName, Serializable id) throws CallbackException;

// beginTransaction() 之后执行
public void afterTransactionBegin(Transaction tx);

// 在事务完成前执行
public void beforeTransactionCompletion(Transaction tx);

// 在事务完成后执行
public void afterTransactionCompletion(Transaction tx);
}
假设您实作了SomeInterceptor类别:
SomeInterceptor.java
package onlyfun.caterpillar;
....
public class SomeInterceptor implements Interceptor {
....
}
在开启Session时,可以如下加载自订的Interceptor:
SomeInterceptor someInterceptor = new SomeInterceptor();
Session session = sessionFactory.openSession(someInterceptor);
....

你可能感兴趣的:(编程相关技术)