【永恒的瞬间】
☜Give me hapy ☞

第 11 章 与对象共事

Hibernate是完整的对象/关系映射解决方案,它提供了对象状态管理(state management)的功能,使开发者不再需要理会底层数据库系统的细节。 也就是说,相对于常见的JDBC/SQL持久层方案中需要管理SQL语句,Hibernate采用了更自然的面向对象的视角来持久化Java应用中的数据。

换句话说,使用Hibernate的开发者应该总是关注对象的状态(state),不必考虑SQL语句的执行。 这部分细节已经由Hibernate掌管妥当,只有开发者在进行系统性能调优的时候才需要进行了解。

11.1. Hibernate对象状态(object states)

Hibernate定义并支持下列对象状态(state):

  • 瞬时(Transient) - 由new操作符创建,且尚未与Hibernate Session 关联的对象被认定为瞬时(Transient)的。瞬时(Transient)对象不会被持久化到数据库中,也不会被赋予持久化标识(identifier)。 如果程序中没有保持对瞬时(Transient)对象的引用,它会被垃圾回收器(garbage collector)销毁。 使用Hibernate Session可以将其变为持久(Persistent)状态。(Hibernate会自动执行必要的SQL语句)

  • 持久(Persistent) - 持久(Persistent)的实例在数据库中有对应的记录,并拥有一个持久化标识(identifier)。 持久(Persistent)的实例可能是刚被保存的,或刚被加载的,无论哪一种,按定义对象都仅在相关联的Session生命周期内的保持这种状态。 Hibernate会检测到处于持久(Persistent)状态的对象的任何改动,在当前操作单元(unit of work)执行完毕时将对象数据(state)与数据库同步(synchronize)。 开发者不需要手动执行UPDATE。将对象从持久(Persistent)状态变成瞬时(Transient)状态同样也不需要手动执行DELETE语句。

  • 脱管(Detached) - 与持久(Persistent)对象关联的Session被关闭后,对象就变为脱管(Detached)的。 对脱管(Detached)对象的引用依然有效,对象可继续被修改。脱管(Detached)对象如果重新关联到某个新的Session上, 会再次转变为持久(Persistent)的(Detached其间的改动将被持久化到数据库)。 这个功能使得一种编程模型,即中间会给用户思考时间(user think-time)的长时间运行的操作单元(unit of work)的编程模型成为可能。 我们称之为应用程序事务,即从用户观点看是一个操作单元(unit of work)。

接下来我们来细致的讨论下状态(states)及状态间的转换(state transitions)(以及触发状态转换的Hibernate方法)。

11.2. 使对象持久化

Hibernate认为持久化类(persistent class)新实例化的对象是瞬时(Transient)的。 我们可将瞬时(Transient)对象与session关联而变为持久(Persistent)的。

DomesticCat fritz = new DomesticCat();
fritz.setColor(Color.GINGER);
fritz.setSex('M');
fritz.setName("Fritz");
Long generatedId = (Long) sess.save(fritz);

如果Cat的持久化标识(identifier)是generated类型的, 那么该标识(identifier)会自动在save()被调用时产生并分配给cat。 如果Cat的持久化标识(identifier)是assigned类型的,或是一个复合主键(composite key), 那么该标识(identifier)应当在调用save()之前手动赋予给cat。 你也可以按照EJB3 early draft中定义的语义,使用persist()替代save()

此外,你可以用一个重载版本的save()方法。

DomesticCat pk = new DomesticCat();
pk.setColor(Color.TABBY);
pk.setSex('F');
pk.setName("PK");
pk.setKittens( new HashSet() );
pk.addKitten(fritz);
sess.save( pk, new Long(1234) );

如果你持久化的对象有关联的对象(associated objects)(例如上例中的kittens集合) 那么对这些对象(译注:pk和kittens)进行持久化的顺序是任意的(也就是说可以先对kittens进行持久化也可以先对pk进行持久化), 除非你在外键列上有NOT NULL约束。 Hibernate不会违反外键约束,但是如果你用错误的顺序持久化对象(译注:在pk持久之前持久kitten),那么可能会违反NOT NULL约束。

通常你不会为这些细节烦心,因为你很可能会使用Hibernate的 传播性持久化(transitive persistence)功能自动保存相关联那些对象。 这样连违反NOT NULL约束情况都不会出现了 - Hibernate会管好所有的事情。 传播性持久化(transitive persistence)将在本章稍后讨论。

11.3. 装载对象

如果你知道某个实例的持久化标识(identifier),你就可以使用Sessionload()方法 来获取它。 load()的另一个参数是指定类的.class对象。 本方法会创建指定类的持久化实例,并从数据库加载其数据(state)。

Cat fritz = (Cat) sess.load(Cat.class, generatedId);
// you need to wrap primitive identifiers
long pkId = 1234;
DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );

此外, 你可以把数据(state)加载到指定的对象实例上(覆盖掉该实例原来的数据)。

Cat cat = new DomesticCat();
// load pk's state into cat
sess.load( cat, new Long(pkId) );
Set kittens = cat.getKittens();

请注意如果没有匹配的数据库记录,load()方法可能抛出无法恢复的异常(unrecoverable exception)。 如果类的映射使用了代理(proxy),load()方法会返回一个未初始化的代理,直到你调用该代理的某方法时才会去访问数据库。 若你希望在某对象中创建一个指向另一个对象的关联,又不想在从数据库中装载该对象时同时装载相关联的那个对象,那么这种操作方式就用得上的了。 如果为相应类映射关系设置了batch-size, 那么使用这种操作方式允许多个对象被一批装载(因为返回的是代理,无需从数据库中抓取所有对象的数据)。

如果你不确定是否有匹配的行存在,应该使用get()方法,它会立刻访问数据库,如果没有对应的行,会返回null。

Cat cat = (Cat) sess.get(Cat.class, id);
if (cat==null) {
cat = new Cat();
sess.save(cat, id);
}
return cat;

你甚至可以选用某个LockMode,用SQL的SELECT ... FOR UPDATE装载对象。 请查阅API文档以获取更多信息。

Cat cat = (Cat) sess.get(Cat.class, id, LockMode.UPGRADE);

注意,任何关联的对象或者包含的集合都不会被以FOR UPDATE方式返回, 除非你指定了lock或者all作为关联(association)的级联风格(cascade style)。

任何时候都可以使用refresh()方法强迫装载对象和它的集合。如果你使用数据库触发器功能来处理对象的某些属性,这个方法就很有用了。

sess.save(cat);
sess.flush(); //force the SQL INSERT
sess.refresh(cat); //re-read the state (after the trigger executes)

此处通常会出现一个重要问题: Hibernate会从数据库中装载多少东西?会执行多少条相应的SQLSELECT语句? 这取决于抓取策略(fetching strategy),会在第 20.1 节 “ 抓取策略(Fetching strategies) ”中解释。

11.4. 查询

如果不知道所要寻找的对象的持久化标识,那么你需要使用查询。Hibernate支持强大且易于使用的面向对象查询语言(HQL)。 如果希望通过编程的方式创建查询,Hibernate提供了完善的按条件(Query By Criteria, QBC)以及按样例(Query By Example, QBE)进行查询的功能。 你也可以用原生SQL(native SQL)描述查询,Hibernate提供了将结果集(result set)转化为对象的部分支持。

11.4.1. 执行查询

HQL和原生SQL(native SQL)查询要通过为org.hibernate.Query的实例来表达。 这个接口提供了参数绑定、结果集处理以及运行实际查询的方法。 你总是可以通过当前Session获取一个Query对象:

List cats = session.createQuery(
"from Cat as cat where cat.birthdate < ?")
.setDate(0, date)
.list();
List mothers = session.createQuery(
"select mother from Cat as cat join cat.mother as mother where cat.name = ?")
.setString(0, name)
.list();
List kittens = session.createQuery(
"from Cat as cat where cat.mother = ?")
.setEntity(0, pk)
.list();
Cat mother = (Cat) session.createQuery(
"select cat.mother from Cat as cat where cat = ?")
.setEntity(0, izi)
.uniqueResult();

一个查询通常在调用list()时被执行,执行结果会完全装载进内存中的一个集合(collection)。 查询返回的对象处于持久(persistent)状态。如果你知道的查询只会返回一个对象,可使用list()的快捷方式uniqueResult()

11.4.1.1. 迭代式获取结果(Iterating results)

某些情况下,你可以使用iterate()方法得到更好的性能。 这通常是你预期返回的结果在session,或二级缓存(second-level cache)中已经存在时的情况。 如若不然,iterate()会比list()慢,而且可能简单查询也需要进行多次数据库访问: iterate()会首先使用1条语句得到所有对象的持久化标识(identifiers),再根据持久化标识执行n条附加的select语句实例化实际的对象。

// fetch ids
Iterator iter = sess.createQuery("from eg.Qux q order by q.likeliness").iterate();
while ( iter.hasNext() ) {
Qux qux = (Qux) iter.next();  // fetch the object
// something we couldnt express in the query
if ( qux.calculateComplicatedAlgorithm() ) {
// delete the current instance
iter.remove();
// dont need to process the rest
break;
}
}

11.4.1.2. 返回元组(tuples)的查询

(译注:元组(tuples)指一条结果行包含多个对象) Hibernate查询有时返回元组(tuples),每个元组(tuples)以数组的形式返回:

Iterator kittensAndMothers = sess.createQuery(
"select kitten, mother from Cat kitten join kitten.mother mother")
.list()
.iterator();
while ( kittensAndMothers.hasNext() ) {
Object[] tuple = (Object[]) kittensAndMothers.next();
Cat kitten  = tuple[0];
Cat mother  = tuple[1];
....
}

11.4.1.3. 标量(Scalar)结果

查询可在select从句中指定类的属性,甚至可以调用SQL统计(aggregate)函数。 属性或统计结果被认定为"标量(Scalar)"的结果(而不是持久(persistent state)的实体)。

Iterator results = sess.createQuery(
"select cat.color, min(cat.birthdate), count(cat) from Cat cat " +
"group by cat.color")
.list()
.iterator();
while ( results.hasNext() ) {
Object[] row = results.next();
Color type = (Color) row[0];
Date oldest = (Date) row[1];
Integer count = (Integer) row[2];
.....
}

11.4.1.4. 绑定参数

接口Query提供了对命名参数(named parameters)、JDBC风格的问号(?)参数进行绑定的方法。 不同于JDBC,Hibernate对参数从0开始计数。 命名参数(named parameters)在查询字符串中是形如:name的标识符。 命名参数(named parameters)的优点是:

  • 命名参数(named parameters)与其在查询串中出现的顺序无关

  • 它们可在同一查询串中多次出现

  • 它们本身是自我说明的

//named parameter (preferred)
Query q = sess.createQuery("from DomesticCat cat where cat.name = :name");
q.setString("name", "Fritz");
Iterator cats = q.iterate();
//positional parameter
Query q = sess.createQuery("from DomesticCat cat where cat.name = ?");
q.setString(0, "Izi");
Iterator cats = q.iterate();
//named parameter list
List names = new ArrayList();
names.add("Izi");
names.add("Fritz");
Query q = sess.createQuery("from DomesticCat cat where cat.name in (:namesList)");
q.setParameterList("namesList", names);
List cats = q.list();

11.4.1.5. 分页

如果你需要指定结果集的范围(希望返回的最大行数/或开始的行数),应该使用Query接口提供的方法:

Query q = sess.createQuery("from DomesticCat cat");
q.setFirstResult(20);
q.setMaxResults(10);
List cats = q.list();

Hibernate 知道如何将这个有限定条件的查询转换成你的数据库的原生SQL(native SQL)。

11.4.1.6. 可滚动遍历(Scrollable iteration)

如果你的JDBC驱动支持可滚动的ResuleSetQuery接口可以使用ScrollableResults,允许你在查询结果中灵活游走。

Query q = sess.createQuery("select cat.name, cat from DomesticCat cat " +
"order by cat.name");
ScrollableResults cats = q.scroll();
if ( cats.first() ) {
// find the first name on each page of an alphabetical list of cats by name
firstNamesOfPages = new ArrayList();
do {
String name = cats.getString(0);
firstNamesOfPages.add(name);
}
while ( cats.scroll(PAGE_SIZE) );
// Now get the first page of cats
pageOfCats = new ArrayList();
cats.beforeFirst();
int i=0;
while( ( PAGE_SIZE > i++ ) && cats.next() ) pageOfCats.add( cats.get(1) );
}
cats.close()

请注意,使用此功能需要保持数据库连接(以及游标(cursor))处于一直打开状态。 如果你需要断开连接使用分页功能,请使用setMaxResult()/setFirstResult()

11.4.1.7. 外置命名查询(Externalizing named queries)

你可以在映射文件中定义命名查询(named queries)。 (如果你的查询串中包含可能被解释为XML标记(markup)的字符,别忘了用CDATA包裹起来。)

<query name="eg.DomesticCat.by.name.and.minimum.weight"><![CDATA[
from eg.DomesticCat as cat
where cat.name = ?
and cat.weight > ?
] ]></query>

参数绑定及执行以编程方式(programatically)完成:

Query q = sess.getNamedQuery("eg.DomesticCat.by.name.and.minimum.weight");
q.setString(0, name);
q.setInt(1, minWeight);
List cats = q.list();

请注意实际的程序代码与所用的查询语言无关,你也可在元数据中定义原生SQL(native SQL)查询, 或将原有的其他的查询语句放在配置文件中,这样就可以让Hibernate统一管理,达到迁移的目的。

11.4.2. 过滤集合

集合过滤器(filter)是一种用于一个持久化集合或者数组的特殊的查询。查询字符串中可以使用"this"来引用集合中的当前元素。

Collection blackKittens = session.createFilter(
pk.getKittens(),
"where this.color = ?")
.setParameter( Color.BLACK, Hibernate.custom(ColorUserType.class) )
.list()
);

返回的集合可以被认为是一个包(bag, 无顺序可重复的集合(collection)),它是所给集合的副本。 原来的集合不会被改动(这与“过滤器(filter)”的隐含的含义不符,不过与我们期待的行为一致)。

请注意过滤器(filter)并不需要from子句(当然需要的话它们也可以加上)。过滤器(filter)不限定于只能返回集合元素本身。

Collection blackKittenMates = session.createFilter(
pk.getKittens(),
"select this.mate where this.color = eg.Color.BLACK.intValue")
.list();

即使无条件的过滤器(filter)也是有意义的。例如,用于加载一个大集合的子集:

Collection tenKittens = session.createFilter(
mother.getKittens(), "")
.setFirstResult(0).setMaxResults(10)
.list();

11.4.3. 条件查询(Criteria queries)

HQL极为强大,但是有些人希望能够动态的使用一种面向对象API创建查询,而非在他们的Java代码中嵌入字符串。对于那部分人来说,Hibernate提供了直观的Criteria查询API。

Criteria crit = session.createCriteria(Cat.class);
crit.add( Expression.eq( "color", eg.Color.BLACK ) );
crit.setMaxResults(10);
List cats = crit.list();

Criteria以及相关的样例(Example)API将会再第 16 章 条件查询(Criteria Queries) 中详细讨论。

11.4.4. 使用原生SQL的查询

你可以使用createSQLQuery()方法,用SQL来描述查询,并由Hibernate处理将结果集转换成对象的工作。 请注意,你可以在任何时候调用session.connection()来获得并使用JDBC Connection对象。 如果你选择使用Hibernate的API, 你必须把SQL别名用大括号包围起来:

List cats = session.createSQLQuery(
"SELECT {cat.*} FROM CAT {cat} WHERE ROWNUM<10",
"cat",
Cat.class
).list();
List cats = session.createSQLQuery(
"SELECT {cat}.ID AS {cat.id}, {cat}.SEX AS {cat.sex}, " +
"{cat}.MATE AS {cat.mate}, {cat}.SUBCLASS AS {cat.class}, ... " +
"FROM CAT {cat} WHERE ROWNUM<10",
"cat",
Cat.class
).list()

和Hibernate查询一样,SQL查询也可以包含命名参数和占位参数。 可以在第 17 章 Native SQL查询找到更多关于Hibernate中原生SQL(native SQL)的信息。

11.5. 修改持久对象

事务中的持久实例(就是通过session装载、保存、创建或者查询出的对象) 被应用程序操作所造成的任何修改都会在Session刷出(flushed)的时候被持久化(本章后面会详细讨论)。 这里不需要调用某个特定的方法(比如update(),设计它的目的是不同的)将你的修改持久化。 所以最直接的更新一个对象的方法就是在Session处于打开状态时load()它,然后直接修改即可:

DomesticCat cat = (DomesticCat) sess.load( Cat.class, new Long(69) );
cat.setName("PK");
sess.flush();  // changes to cat are automatically detected and persisted

有时这种程序模型效率低下,因为它在同一Session里需要一条SQL SELECT语句(用于加载对象) 以及一条SQL UPDATE语句(持久化更新的状态)。 为此Hibernate提供了另一种途径,使用脱管(detached)实例。

请注意Hibernate本身不提供直接执行UPDATEDELETE语句的API。 Hibernate提供的是状态管理(state management)服务,你不必考虑要使用的语句(statements)。 JDBC是出色的执行SQL语句的API,任何时候调用session.connection()你都可以得到一个JDBC Connection对象。 此外,在联机事务处理(OLTP)程序中,大量操作(mass operations)与对象/关系映射的观点是相冲突的。 Hibernate的将来版本可能会提供专门的进行大量操作(mass operation)的功能。 参考第 14 章 批量处理(Batch processing),寻找一些可用的批量(batch)操作技巧。

11.6. 修改脱管(Detached)对象

很多程序需要在某个事务中获取对象,然后将对象发送到界面层去操作,最后在一个新的事务保存所做的修改。 在高并发访问的环境中使用这种方式,通常使用附带版本信息的数据来保证这些“长“工作单元之间的隔离。

Hibernate通过提供使用Session.update()Session.merge()方法 重新关联脱管实例的办法来支持这种模型。

// in the first session
Cat cat = (Cat) firstSession.load(Cat.class, catId);
Cat potentialMate = new Cat();
firstSession.save(potentialMate);
// in a higher layer of the application
cat.setMate(potentialMate);
// later, in a new session
secondSession.update(cat);  // update cat
secondSession.update(mate); // update mate

如果具有catId持久化标识的Cat之前已经被另一Session(secondSession)装载了, 应用程序进行重关联操作(reattach)的时候会抛出一个异常。

如果你确定当前session没有包含与之具有相同持久化标识的持久实例,使用update()。 如果想随时合并你的的改动而不考虑session的状态,使用merge()。 换句话说,在一个新session中通常第一个调用的是update()方法,以便保证重新关联脱管(detached)对象的操作首先被执行。

希望相关联的脱管对象(通过引用“可到达”的脱管对象)的数据也要更新到数据库时(并且也仅仅在这种情况), 应用程序需要对该相关联的脱管对象单独调用update() 当然这些可以自动完成,即通过使用传播性持久化(transitive persistence),请看第 11.11 节 “传播性持久化(transitive persistence)”

lock()方法也允许程序重新关联某个对象到一个新session上。不过,该脱管(detached)的对象必须是没有修改过的!

//just reassociate:
sess.lock(fritz, LockMode.NONE);
//do a version check, then reassociate:
sess.lock(izi, LockMode.READ);
//do a version check, using SELECT ... FOR UPDATE, then reassociate:
sess.lock(pk, LockMode.UPGRADE);

请注意,lock()可以搭配多种LockMode, 更多信息请阅读API文档以及关于事务处理(transaction handling)的章节。重新关联不是lock()的唯一用途。

其他用于长时间工作单元的模型会在第 12.3 节 “乐观并发控制(Optimistic concurrency control)”中讨论。

11.7. 自动状态检测

Hibernate的用户曾要求一个既可自动分配新持久化标识(identifier)保存瞬时(transient)对象,又可更新/重新关联脱管(detached)实例的通用方法。 saveOrUpdate()方法实现了这个功能。

// in the first session
Cat cat = (Cat) firstSession.load(Cat.class, catID);
// in a higher tier of the application
Cat mate = new Cat();
cat.setMate(mate);
// later, in a new session
secondSession.saveOrUpdate(cat);   // update existing state (cat has a non-null id)
secondSession.saveOrUpdate(mate);  // save the new instance (mate has a null id)

saveOrUpdate()用途和语义可能会使新用户感到迷惑。 首先,只要你没有尝试在某个session中使用来自另一session的实例,你应该就不需要使用update()saveOrUpdate(),或merge()。有些程序从来不用这些方法。

通常下面的场景会使用update()saveOrUpdate()

  • 程序在第一个session中加载对象

  • 该对象被传递到表现层

  • 对象发生了一些改动

  • 该对象被返回到业务逻辑层

  • 程序调用第二个session的update()方法持久这些改动

saveOrUpdate()做下面的事:

  • 如果对象已经在本session中持久化了,不做任何事

  • 如果另一个与本session关联的对象拥有相同的持久化标识(identifier),抛出一个异常

  • 如果对象没有持久化标识(identifier)属性,对其调用save()

  • 如果对象的持久标识(identifier)表明其是一个新实例化的对象,对其调用save()

  • 如果对象是附带版本信息的(通过<version><timestamp>) 并且版本属性的值表明其是一个新实例化的对象,save()它。

  • 否则update() 这个对象

merge()可非常不同:

  • 如果session中存在相同持久化标识(identifier)的实例,用用户给出的对象的状态覆盖旧有的持久实例

  • 如果session没有相应的持久实例,则尝试从数据库中加载,或创建新的持久化实例

  • 最后返回该持久实例

  • 用户给出的这个对象没有被关联到session上,它依旧是脱管的

11.8. 删除持久对象

使用Session.delete()会把对象的状态从数据库中移除。 当然,你的应用程序可能仍然持有一个指向已删除对象的引用。所以,最好这样理解:delete()的用途是把一个持久实例变成瞬时(transient)实例。

sess.delete(cat);

你可以用你喜欢的任何顺序删除对象,不用担心外键约束冲突。当然,如果你搞错了顺序,还是有可能引发在外键字段定义的NOT NULL约束冲突。 例如你删除了父对象,但是忘记删除孩子们。

11.9. 在两个不同数据库间复制对象

偶尔会用到不重新生成持久化标识(identifier),将持久实例以及其关联的实例持久到不同的数据库中的操作。

//retrieve a cat from one database
Session session1 = factory1.openSession();
Transaction tx1 = session1.beginTransaction();
Cat cat = session1.get(Cat.class, catId);
tx1.commit();
session1.close();
//reconcile with a second database
Session session2 = factory2.openSession();
Transaction tx2 = session2.beginTransaction();
session2.replicate(cat, ReplicationMode.LATEST_VERSION);
tx2.commit();
session2.close();

ReplicationMode决定数据库中已存在相同行时,replicate()如何处理。

  • ReplicationMode.IGNORE - 忽略它

  • ReplicationMode.OVERWRITE - 覆盖相同的行

  • ReplicationMode.EXCEPTION - 抛出异常

  • ReplicationMode.LATEST_VERSION - 如果当前的版本较新,则覆盖,否则忽略

这个功能的用途包括使录入的数据在不同数据库中一致,产品升级时升级系统配置信息,回滚non-ACID事务中的修改等等。 (译注,non-ACID,非ACID;ACID,Atomic,Consistent,Isolated and Durable的缩写)

11.10. Session刷出(flush)

每间隔一段时间,Session会执行一些必需的SQL语句来把内存中的对象的状态同步到JDBC连接中。这个过程被称为刷出(flush),默认会在下面的时间点执行:

  • 在某些查询执行之前

  • 在调用org.hibernate.Transaction.commit()的时候

  • 在调用Session.flush()的时候

涉及的SQL语句会按照下面的顺序发出执行:

  1. 所有对实体进行插入的语句,其顺序按照对象执行Session.save()的时间顺序

  2. 所有对实体进行更新的语句

  3. 所有进行集合删除的语句

  4. 所有对集合元素进行删除,更新或者插入的语句

  5. 所有进行集合插入的语句

  6. 所有对实体进行删除的语句,其顺序按照对象执行Session.delete()的时间顺序

(有一个例外是,如果对象使用native方式来生成ID(持久化标识)的话,它们一执行save就会被插入。)

除非你明确地发出了flush()指令,关于Session何时会执行这些JDBC调用是完全无法保证的,只能保证它们执行的前后顺序。 当然,Hibernate保证,Query.list(..)绝对不会返回已经失效的数据,也不会返回错误数据。

也可以改变默认的设置,来让刷出(flush)操作发生的不那么频繁。 FlushMode类定义了三种不同的方式。 仅在提交时刷出(仅当Hibernate的Transaction API被使用时有效), 按照刚才说的方式刷出, 以及除非明确使用flush()否则从不刷出。 最后一种模式对于那些需要长时间保持Session为打开或者断线状态的长时间运行的工作单元很有用。 (参见 第 12.3.2 节 “长生命周期session和自动版本化”).

sess = sf.openSession();
Transaction tx = sess.beginTransaction();
sess.setFlushMode(FlushMode.COMMIT); // allow queries to return stale state
Cat izi = (Cat) sess.load(Cat.class, id);
izi.setName(iznizi);
// might return stale data
sess.find("from Cat as cat left outer join cat.kittens kitten");
// change to izi is not flushed!
...
tx.commit(); // flush occurs

刷出(flush)期间,可能会抛出异常。(例如一个DML操作违反了约束) 异常处理涉及到对Hibernate事务性行为的理解,因此我们将在第 12 章 事务和并发中讨论。

11.11. 传播性持久化(transitive persistence)

对每一个对象都要执行保存,删除或重关联操作让人感觉有点麻烦,尤其是在处理许多彼此关联的对象的时候。 一个常见的例子是父子关系。考虑下面的例子:

如果一个父子关系中的子对象是值类型(value typed)(例如,地址或字符串的集合)的,他们的生命周期会依赖于父对象,可以享受方便的级联操作(Cascading),不需要额外的动作。 父对象被保存时,这些值类型(value typed)子对象也将被保存;父对象被删除时,子对象也将被删除。 这对将一个子对象从集合中移除是同样有效:Hibernate会检测到,并且因为值类型(value typed)的对象不可能被其他对象引用,所以Hibernate会在数据库中删除这个子对象。

现在考虑同样的场景,不过父子对象都是实体(entities)类型,而非值类型(value typed)(例如,类别与个体,或母猫和小猫)。 实体有自己的生命期,允许共享对其的引用(因此从集合中移除一个实体,不意味着它可以被删除), 并且实体到其他关联实体之间默认没有级联操作的设置。 Hibernate默认不实现所谓的可到达即持久化(persistence by reachability)的策略。

每个Hibernate session的基本操作 - 包括 persist(), merge(), saveOrUpdate(), delete(), lock(), refresh(), evict(), replicate() - 都有对应的级联风格(cascade style)。 这些级联风格(cascade style)风格分别命名为 create, merge, save-update, delete, lock, refresh, evict, replicate。 如果你希望一个操作被顺着关联关系级联传播,你必须在映射文件中指出这一点。例如:

<one-to-one name="person" cascade="persist"/>

级联风格(cascade style)是可组合的:

<one-to-one name="person" cascade="persist,delete,lock"/>

你可以使用cascade="all"来指定全部操作都顺着关联关系级联(cascaded)。 默认值是cascade="none",即任何操作都不会被级联(cascaded)。

注意有一个特殊的级联风格(cascade style) delete-orphan,只应用于one-to-many关联,表明delete()操作 应该被应用于所有从关联中删除的对象。

建议:

  • 通常在<many-to-one><many-to-many>关系中应用级联(cascade)没什么意义。 级联(cascade)通常在 <one-to-one><one-to-many>关系中比较有用。

  • 如果子对象的寿命限定在父亲对象的寿命之内,可通过指定cascade="all,delete-orphan"将其变为自动生命周期管理的对象(lifecycle object)

  • 其他情况,你可根本不需要级联(cascade)。但是如果你认为你会经常在某个事务中同时用到父对象与子对象,并且你希望少打点儿字,可以考虑使用cascade="persist,merge,save-update"

可以使用cascade="all"将一个关联关系(无论是对值对象的关联,或者对一个集合的关联)标记为父/子关系的关联。 这样对父对象进行save/update/delete操作就会导致子对象也进行save/update/delete操作。

此外,一个持久的父对象对子对象的浅引用(mere reference)会导致子对象被同步save/update。 不过,这个隐喻(metaphor)的说法并不完整。除非关联是<one-to-many>关联并且被标记为cascade="delete-orphan", 否则父对象失去对某个子对象的引用不会导致该子对象被自动删除。 父子关系的级联(cascading)操作准确语义如下:

  • 如果父对象被persist(),那么所有子对象也会被persist()

  • 如果父对象被merge(),那么所有子对象也会被merge()

  • 如果父对象被save()update()saveOrUpdate(),那么所有子对象则会被saveOrUpdate()

  • 如果某个持久的父对象引用了瞬时(transient)或者脱管(detached)的子对象,那么子对象将会被saveOrUpdate()

  • 如果父对象被删除,那么所有子对象也会被delete()

  • 除非被标记为cascade="delete-orphan"(删除“孤儿”模式,此时不被任何一个父对象引用的子对象会被删除), 否则子对象失掉父对象对其的引用时,什么事也不会发生。 如果有特殊需要,应用程序可通过显式调用delete()删除子对象。

11.12. 使用元数据

Hibernate中有一个非常丰富的元级别(meta-level)的模型,含有所有的实体和值类型数据的元数据。 有时这个模型对应用程序本身也会非常有用。 比如说,应用程序可能在实现一种“智能”的深度拷贝算法时, 通过使用Hibernate的元数据来了解哪些对象应该被拷贝(比如,可变的值类型数据), 那些不应该(不可变的值类型数据,也许还有某些被关联的实体)。

Hibernate提供了ClassMetadata接口,CollectionMetadata接口和Type层次体系来访问元数据。 可以通过SessionFactory获取元数据接口的实例。

Cat fritz = ......;
ClassMetadata catMeta = sessionfactory.getClassMetadata(Cat.class);
Object[] propertyValues = catMeta.getPropertyValues(fritz);
String[] propertyNames = catMeta.getPropertyNames();
Type[] propertyTypes = catMeta.getPropertyTypes();
// get a Map of all properties which are not collections or associations
Map namedValues = new HashMap();
for ( int i=0; i<propertyNames.length; i++ ) {
if ( !propertyTypes[i].isEntityType() && !propertyTypes[i].isCollectionType() ) {
namedValues.put( propertyNames[i], propertyValues[i] );
}
}
}

第 12 章 事务和并发

Hibernate的事务和并发控制很容易掌握。Hibernate直接使用JDBC连接和JTA资源,不添加任何附加锁定 行为。我们强烈推荐你花点时间了解JDBC编程,ANSI SQL查询语言和你使用 的数据库系统的事务隔离规范。Hibernate只添加自动版本管理,而不会锁 定内存中的对象,也不会改变数据库事务的隔离级别。基本上,使用 Hibernate就好像直接使用JDBC(或者JTA/CMT)来访问你的数据库资源。

除了自动版本管理,针对行级悲观锁定,Hibernate也提供了辅助的API,它使用了 SELECT FOR UPDATE的SQL语法。本章后面会讨论这个API。

我们从Configuration层、SessionFactory层, 和 Session层开始讨论Hibernate的并行控制、数据库事务和应用 程序的长事务。

12.1. Session和事务范围(transaction scopes)

一个SessionFactory对象的创建代价很昂贵,它是线程安全的对象,它被设计成可以 为所有的应用程序线程所共享。它只创建一次,通常是在应用程序启动的时候,由一个 Configuraion的实例来创建。

一个Session的对象是轻型的,非线程安全的,对于单个业务进程,单个的 工作单元而言,它只被使用一次,然后就丢弃。只有在需要的时候,Session 才会获取一个JDBC的Connection(或一个Datasource) 对象。所以你可以放心的打开和关闭Session,甚至当你并不确定一个特定的请 求是否需要数据访问时,你也可以这样做。(一旦你实现下面提到的使用了请求拦截的模式,这就 变得很重要了。

此外我们还要考虑数据库事务。数据库事务应该尽可能的短,降低数据库锁定造成的资源争用。 数据库长事务会导致你的应用程序无法扩展到高的并发负载。

一个操作单元(Unit of work)的范围是多大?单个的Hibernate Session能跨越多个 数据库事务吗?还是一个Session的作用范围对应一个数据库事务的范围?应该何时打开 Session,何时关闭Session?,你又如何划分数据库事务的边界呢?

12.1.1. 操作单元(Unit of work)

首先,别再用session-per-operation这种反模式了,也就是说,在单个线程中, 不要因为一次简单的数据库调用,就打开和关闭一次Session!数据库事务也是如此。 应用程序中的数据库调用是按照计划好的次序,分组为原子的操作单元。(注意,这也意味着,应用程 序中,在单个的SQL语句发送之后,自动事务提交(auto-commit)模式失效了。这种模式专门为SQL控制台操作设计的。 Hibernate禁止立即自动事务提交模式,或者期望应用服务器禁止立即自动事务提交模式。)

在多用户的client/server应用程序中,最常用的模式是 每个请求一个会话(session-per-request)。 在这种模式下,来自客户端的请求被发送到服务器端(即Hibernate持久化层运行的地方),一 个新的Hibernate Session被打开,并且执行这个操作单元中所有的数据库操作。 一旦操作完成(同时发送到客户端的响应也准备就绪),session被同步,然后关闭。你也可以使用单 个数据库事务来处理客户端请求,在你打开Session之后启动事务,在你关闭 Session之前提交事务。会话和请求之间的关系是一对一的关系,这种模式对 于大多数应用程序来说是很棒的。

真正的挑战在于如何去实现这种模式:不仅Session和事务必须被正确的开始和结束, 而且他们也必须能被数据访问操作访问。用拦截器来实现操作单元的划分,该拦截器在客户端请求达到服 务器端的时候开始,在服务器端发送响应(即,ServletFilter)之前结束。我们推荐 使用一个ThreadLocal 变量,把 Session绑定到处理客户端请求的线 程上去。这种方式可以让运行在该线程上的所有程序代码轻松的访问Session(就像访问一 个静态变量那样)。你也可以在一个ThreadLocal 变量中保持事务上下文环境,不过这依赖 于你所选择的数据库事务划分机制。这种实现模式被称之为 ThreadLocal SessionOpen Session in View。你可以很容易的扩展本文前面章节展示的 HibernateUtil 辅助类来实现这种模式。当然,你必须找到一种实现拦截器的方法,并 且可以把拦截器集成到你的应用环境中。请参考Hibernate网站上面的提示和例子。

12.1.2. 应用程序事务(Application transactions)

session-per-request模式不仅仅是一个可以用来设计操作单元的有用概念。很多业务处理流程都需 要一系列完整的和用户之间的交互,即用户对数据库的交叉访问。在基于web的应用和企业 应用中,跨用户交互的数据库事务是无法接受的。考虑下面的例子:

  • 在界面的第一屏,打开对话框,用户所看到的数据是被一个特定的 Session 和数据 库事务载入(load)的。用户可以随意修改对话框中的数据对象。

  • 5分钟后,用户点击“保存”,期望所做出的修改被持久化;同时他也期望自己是唯一修改这个信息的人,不会出现 修改冲突。

从用户的角度来看,我们把这个操作单元称为应用程序长事务(application transaction)。 在你的应用程序中,可以有很多种方法来实现它。

头一个幼稚的做法是,在用户思考的过程中,保持Session和数据库事务是打开的, 保持数据库锁定,以阻止并发修改,从而保证数据库事务隔离级别和原子操作。这种方式当然是一个反模式, 因为数据库锁定的维持会导致应用程序无法扩展并发用户的数目。

很明显,我们必须使用多个数据库事务来实现一个应用程序事务。在这个例子中,维护业务处理流程的 事务隔离变成了应用程序层的部分责任。单个应用程序事务通常跨越多个数据库事务。如果仅仅只有一 个数据库事务(最后的那个事务)保存更新过的数据,而所有其他事务只是单纯的读取数据(例如在一 个跨越多个请求/响应周期的向导风格的对话框中),那么应用程序事务将保证其原子性。这种方式比听 起来还要容易实现,特别是当你使用了Hibernate的下述特性的时候:

  • 自动版本化 - Hibernate能够自动进行乐观并发控制 ,如果在用户思考 的过程中发生并发修改冲突,Hibernate能够自动检测到。

  • 脱管对象(Detached Objects)- 如果你决定采用前面已经讨论过的 session-per-request模式,所有载入的实例在用户思考的过程 中都处于与Session脱离的状态。Hibernate允许你把与Session脱离的对象重新关联到Session 上,并且对修改进行持久化,这种模式被称为 session-per-request-with-detached-objects。自动版本化被用来隔离并发修改。

  • 长生命周期的Session (Long Session)- Hibernate 的Session 可以在数据库事务提交之后和底层的JDBC连接断开,当一个新的客户端请求到来的时候,它又重新连接上底层的 JDBC连接。这种模式被称之为session-per-application-transaction,这种情况可 能会造成不必要的Session和JDBC连接的重新关联。自动版本化被用来隔离并发修改。

session-per-request-with-detached-objectssession-per-application-transaction 各有优缺点,我们在本章后面乐观并发 控制那部分再进行讨论。

12.1.3. 关注对象标识(Considering object identity)

应用程序可能在两个不同的Session中并发访问同一持久化状态,但是, 一个持久化类的实例无法在两个 Session中共享。因此有两种不同的标识语义:

 

数据库标识

foo.getId().equals( bar.getId() )

JVM 标识

foo==bar

 

对于那些关联到 特定Session (也就是在单个Session的范围内)上的对象来说,这 两种标识的语义是等价的,与数据库标识对应的JVM标识是由Hibernate来保 证的。不过,当应用程序在两个不同的session中并发访问具有同一持久化标 识的业务对象实例的时候,这个业务对象的两个实例事实上是不相同的(从 JVM识别来看)。这种冲突可以通过在同步和提交的时候使用自动版本化和乐 观锁定方法来解决。

 

这种方式把关于并发的头疼问题留给了Hibernate和数据库;由于在单个线程内,操作单元中的对象识别不 需要代价昂贵的锁定或其他意义上的同步,因此它同时可以提供最好的可伸缩性。只要在单个线程只持有一个 Session,应用程序就不需要同步任何业务对象。在Session 的范围内,应用程序可以放心的使用==进行对象比较。

 

不过,应用程序在Session的外面使用==进行对象比较可能会 导致无法预期的结果。在一些无法预料的场合,例如,如果你把两个脱管对象实例放进同一个 Set的时候,就可能发生。这两个对象实例可能有同一个数据库标识(也就是说, 他们代表了表的同一行数据),从JVM标识的定义上来说,对脱管的对象而言,Hibernate无法保证他们 的的JVM标识一致。开发人员必须覆盖持久化类的equals()方法和 hashCode() 方法,从而实现自定义的对象相等语义。警告:不要使用数据库标识 来实现对象相等,应该使用业务键值,由唯一的,通常不变的属性组成。当一个瞬时对象被持久化的时 候,它的数据库标识会发生改变。如果一个瞬时对象(通常也包括脱管对象实例)被放入一 个Set,改变它的hashcode会导致与这个Set的关系中断。虽 然业务键值的属性不象数据库主键那样稳定不变,但是你只需要保证在同一个Set 中的对象属性的稳定性就足够了。请到Hibernate网站去寻求这个问题更多的详细的讨论。请注意,这不是一 个有关Hibernate的问题,而仅仅是一个关于Java对象标识和判等行为如何实现的问题。

 

12.1.4. 常见问题

决不要使用反模式session-per-user-session或者 session-per-application(当然,这个规定几乎没有例外)。请注意, 下述一些问题可能也会出现在我们推荐的模式中,在你作出某个设计决定之前,请务必理解该模式的应用前提。

  • Session 是一个非线程安全的类。如果一个Session 实例允许共享的话,那些支持并发运行的东东,例如HTTP request,session beans,或者是 Swing workers,将会导致出现资源争用(race condition)。如果在HttpSession中有 Hibernate 的Session的话(稍后讨论),你应该考虑同步访问你的Http session。 否则,只要用户足够快的点击浏览器的“刷新”,就会导致两个并发运行线程使用同一个 Session

  • 一个由Hibernate抛出的异常意味着你必须立即回滚数据库事务,并立即关闭Session (稍后会展开讨论)。如果你的Session绑定到一个应用程序上,你必 须停止该应用程序。回滚数据库事务并不会把你的业务对象退回到事务启动时候的状态。这 意味着数据库状态和业务对象状态不同步。通常情况下,这不是什么问题,因为异常是不可 恢复的,你必须在回滚之后重新开始执行。

  • Session 缓存了处于持久化状态的每个对象(Hibernate会监视和检查脏数据)。 这意味着,如果你让Session打开很长一段时间,或是仅仅载入了过多的数据, Session占用的内存会一直增长,直到抛出OutOfMemoryException异常。这个 问题的一个解决方法是调用clear()evict()来管理 Session的缓存,但是如果你需要大批量数据操作的话,最好考虑 使用存储过程。在第 14 章 批量处理(Batch processing)中有一些解决方案。在用户会话期间一直保持 Session打开也意味着出现脏数据的可能性很高。

12.2. 数据库事务声明

数据库(或者系统)事务的声明总是必须的。在数据库事务之外,就无法和数据库通讯(这可能会让那些习惯于 自动提交事务模式的开发人员感到迷惑)。永远使用清晰的事务声明,即使只读操作也是如此。进行 显式的事务声明并不总是需要的,这取决于你的事务隔离级别和数据库的能力,但不管怎么说,声明事务总归有益无害。

一个Hibernate应用程序可以运行在非托管环境中(也就是独立运行的应用程序,简单Web应用程序, 或者Swing图形桌面应用程序),也可以运行在托管的J2EE环境中。在一个非托管环境中,Hibernate 通常自己负责管理数据库连接池。应用程序开发人员必须手工设置事务声明,换句话说,就是手工启 动,提交,或者回滚数据库事务。一个托管的环境通常提供了容器管理事务,例如事务装配通过可声 明的方式定义在EJB session beans的部署描述符中。可编程式事务声明不再需要,即使是 Session 的同步也可以自动完成。

让持久层具备可移植性是人们的理想。Hibernate提供了一套称为Transaction的封装API, 用来把你的部署环境中的本地事务管理系统转换到Hibernate事务上。这个API是可选的,但是我们强烈 推荐你使用,除非你用CMT session bean。

通常情况下,结束 Session 包含了四个不同的阶段:

  • 同步session(flush,刷出到磁盘)

  • 提交事务

  • 关闭session

  • 处理异常

session的同步(flush,刷出)前面已经讨论过了,我们现在进一步考察在托管和非托管环境下的事务声明和异常处理。

12.2.1. 非托管环境

如果Hibernat持久层运行在一个非托管环境中,数据库连接通常由Hibernate的连接池机制 来处理。session/transaction处理方式如下所示:

//Non-managed environment idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
tx = sess.beginTransaction();
// do some work
...
tx.commit();
}
catch (RuntimeException e) {
if (tx != null) tx.rollback();
throw e; // or display error message
}
finally {
sess.close();
}

你不需要显式flush() Session - 对commit()的调用会自动触发session的同步。

调用 close() 标志session的结束。 close()方法重要的暗示是,session释放了JDBC连接。

这段Java代码是可移植的,可以在非托管环境和JTA环境中运行。

你很可能从未在一个标准的应用程序的业务代码中见过这样的用法;致命的(系统)异常应该总是 在应用程序“顶层”被捕获。换句话说,执行Hibernate调用的代码(在持久层)和处理 RuntimeException异常的代码(通常只能清理和退出应用程序)应该在不同 的应用程序逻辑层。这对于你设计自己的软件系统来说是一个挑战,只要有可能,你就应该使用 J2EE/EJB容器服务。异常处理将在本章稍后进行讨论。

请注意,你应该选择 org.hibernate.transaction.JDBCTransactionFactory (这是默认选项).

12.2.2. 使用JTA

如果你的持久层运行在一个应用服务器中(例如,在EJB session beans的后面),Hibernate获取 的每个数据源连接将自动成为全局JTA事务的一部分。Hibernate提供了两种策略进行JTA集成。

如果你使用bean管理事务(BMT),可以通过使用Hibernate的 Transaction API来告诉 应用服务器启动和结束BMT事务。因此,事务管理代码和在非托管环境下是一样的。

// BMT idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
tx = sess.beginTransaction();
// do some work
...
tx.commit();
}
catch (RuntimeException e) {
if (tx != null) tx.rollback();
throw e; // or display error message
}
finally {
sess.close();
}

在CMT方式下,事务声明是在session bean的部署描述符中,而不需要编程。 除非你设置了属性hibernate.transaction.flush_before_completionhibernate.transaction.auto_close_sessiontrue, 否则你必须自己同步和关闭Session。Hibernate可以为你自动同步和关闭 Session。你唯一要做的就是当发生异常时进行事务回滚。幸运的是, 在一个CMT bean中,事务回滚甚至可以由容器自动进行,因为由session bean方法抛出的未处理的 RuntimeException异常可以通知容器设置全局事务回滚。这意味着 在CMT中,你完全无需使用Hibernate的Transaction API 。

请注意,当你配置Hibernate事务工厂的时候,在一个BMT session bean中,你应该选择 org.hibernate.transaction.JTATransactionFactory,在一个 CMT session bean中选择org.hibernate.transaction.CMTTransactionFactory。 记住,同时也要设置org.hibernate.transaction.manager_lookup_class

如果你使用CMT环境,并且让容器自动同步和关闭session,你可能也希望在你代码的不同部分使用 同一个session。一般来说,在一个非托管环境中,你可以使用一个ThreadLocal 变量来持有这个session,但是单个EJB方法调用可能会在不同的线程中执行(举例来说,一个session bean调用另一个session bean)。如果你不想在应用代码中被传递Session对 象实例的问题困扰的话,那么SessionFactory 提供的 getCurrentSession()方法就很适合你,该方法返回一个绑定到JTA事务 上下文环境中的session实例。这也是把Hibernate集成到一个应用程序中的最简单的方法!这个“当 前的”session总是可以自动同步和自动关闭(不考虑上述的属性设置)。我们的session/transaction 管理代码减少到如下所示:

// CMT idiom
Session sess = factory.getCurrentSession();
// do some work
...

换句话来说,在一个托管环境下,你要做的所有的事情就是调用 SessionFactory.getCurrentSession(),然后进行你的数据访问,把其余的工作 交给容器来做。事务在你的session bean的部署描述符中以可声明的方式来设置。session的生命周期完全 由Hibernate来管理。

after_statement连接释放方式有一个警告。因为JTA规范的一个很愚蠢的限制,Hibernate不可能自动清理任何未关闭的ScrollableResults 或者Iterator,它们是由scroll()iterate()产生的。你must通过在finally块中,显式调用ScrollableResults.close()或者Hibernate.close(Iterator)方法来释放底层数据库游标。(当然,大部分程序完全可以很容易的避免在CMT代码中出现scroll()iterate()。)

12.2.3. 异常处理

如果 Session 抛出异常 (包括任何SQLException), 你应该立即回滚数据库事务,调用 Session.close() ,丢弃该 Session实例。Session的某些方法可能会导致session 处于不一致的状态。所有由Hibernate抛出的异常都视为不可以恢复的。确保在 finally 代码块中调用close()方法,以关闭掉 Session

HibernateException是一个非检查期异常(这不同于Hibernate老的版本), 它封装了Hibernate持久层可能出现的大多数错误。我们的观点是,不应该强迫应用程序开发人员 在底层捕获无法恢复的异常。在大多数软件系统中,非检查期异常和致命异常都是在相应方法调用 的堆栈的顶层被处理的(也就是说,在软件上面的逻辑层),并且提供一个错误信息给应用软件的用户 (或者采取其他某些相应的操作)。请注意,Hibernate也有可能抛出其他并不属于 HibernateException的非检查期异常。这些异常同样也是无法恢复的,应该 采取某些相应的操作去处理。

在和数据库进行交互时,Hibernate把捕获的SQLException封装为Hibernate的 JDBCException。事实上,Hibernate尝试把异常转换为更有实际含义 的JDBCException异常的子类。底层的SQLException可以 通过JDBCException.getCause()来得到。Hibernate通过使用关联到 SessionFactory上的SQLExceptionConverter来 把SQLException转换为一个对应的JDBCException 异常的子类。默认情况下,SQLExceptionConverter可以通过配置dialect 选项指定;此外,也可以使用用户自定义的实现类(参考javadocs SQLExceptionConverterFactory类来了解详情)。标准的 JDBCException子类型是:

  • JDBCConnectionException - 指明底层的JDBC通讯出现错误

  • SQLGrammarException - 指明发送的SQL语句的语法或者格式错误

  • ConstraintViolationException - 指明某种类型的约束违例错误

  • LockAcquisitionException - 指明了在执行请求操作时,获取 所需的锁级别时出现的错误。

  • GenericJDBCException - 不属于任何其他种类的原生异常

12.3. 乐观并发控制(Optimistic concurrency control)

唯一能够同时保持高并发和高可伸缩性的方法就是使用带版本化的乐观并发控制。版本检查使用版本号、 或者时间戳来检测更新冲突(并且防止更新丢失)。Hibernate为使用乐观并发控制的代码提供了三种可 能的方法,应用程序在编写这些代码时,可以采用它们。我们已经在前面应用程序长事务那部分展示了 乐观并发控制的应用场景,此外,在单个数据库事务范围内,版本检查也提供了防止更新丢失的好处。

12.3.1. 应用程序级别的版本检查(Application version checking)

未能充分利用Hibernate功能的实现代码中,每次和数据库交互都需要一个新的 Session,而且开发人员必须在显示数据之前从数据库中重 新载入所有的持久化对象实例。这种方式迫使应用程序自己实现版本检查来确保 应用程序事务的隔离,从数据访问的角度来说是最低效的。这种使用方式和 entity EJB最相似。

// foo is an instance loaded by a previous Session
session = factory.openSession();
Transaction t = session.beginTransaction();
int oldVersion = foo.getVersion();
session.load( foo, foo.getKey() ); // load the current state
if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();
foo.setProperty("bar");
t.commit();
session.close();

version 属性使用 <version>来映射,如果对象 是脏数据,在同步的时候,Hibernate会自动增加版本号。

当然,如果你的应用是在一个低数据并发环境下,并不需要版本检查的话,你照样可以使用 这种方式,只不过跳过版本检查就是了。在这种情况下,最晚提交生效last commit wins)就是你的应用程序长事务的默认处理策略。 请记住这种策略可能会让应用软件的用户感到困惑,因为他们有可能会碰上更新丢失掉却没 有出错信息,或者需要合并更改冲突的情况。

很明显,手工进行版本检查只适合于某些软件规模非常小的应用场景,对于大多数软件应用场景 来说并不现实。通常情况下,不仅是单个对象实例需要进行版本检查,整个被修改过的关 联对象图也都需要进行版本检查。作为标准设计范例,Hibernate使用长生命周期 Session的方式,或者脱管对象实例的方式来提供自动版本检查。

12.3.2. 长生命周期session和自动版本化

单个 Session实例和它所关联的所有持久化对象实例都被用于整个 应用程序事务。Hibernate在同步的时候进行对象实例的版本检查,如果检测到并发修 改则抛出异常。由开发人员来决定是否需要捕获和处理这个异常(通常的抉择是给用户 提供一个合并更改,或者在无脏数据情况下重新进行业务操作的机会)。

在等待用户交互的时候, Session 断开底层的JDBC连接。这种方式 以数据库访问的角度来说是最高效的方式。应用程序不需要关心版本检查或脱管对象实例 的重新关联,在每个数据库事务中,应用程序也不需要载入读取对象实例。

// foo is an instance loaded earlier by the Session
session.reconnect(); // Obtain a new JDBC connection
Transaction t = session.beginTransaction();
foo.setProperty("bar");
t.commit(); // End database transaction, flushing the change and checking the version
session.disconnect(); // Return JDBC connection 

foo 对象始终和载入它的Session相关联。 Session.reconnect()获取一个新的数据库连接(或者 你可以提供一个),并且继续当前的session。Session.disconnect() 方法把session与JDBC连接断开,把数据库连接返回到连接池(除非是你自己提供的数据 库连接)。在Session重新连接上数据库连接之后,你可以对任何可能被其他事务更新过 的对象调用Session.lock(),设置LockMode.READ 锁定模式,这样你就可以对那些你不准备更新的数据进行强制版本检查。此外,你并不需要 锁定那些你准备更新的数据。

假若对disconnect()reconnect()的显式调用发生得太频繁了,你可以使用hibernate.connection.release_mode来代替。

如果在用户思考的过程中,Session因为太大了而不能保存,那么这种模式是有 问题的。举例来说,一个HttpSession应该尽可能的小。由于 Session是一级缓存,并且保持了所有被载入过的对象,因此 我们只应该在那些少量的request/response情况下使用这种策略。而且在这种情况下, Session 里面很快就会有脏数据出现,因此请牢牢记住这一建议。

此外,也请注意,你应该让与数据库连接断开的Session对持久层保持 关闭状态。换句话说,使用有状态的EJB session bean来持有Session, 而不要把它传递到web层(甚至把它序列化到一个单独的层),保存在HttpSession中。

12.3.3. 脱管对象(deatched object)和自动版本化

这种方式下,与持久化存储的每次交互都发生在一个新的Session中。 然而,同一持久化对象实例可以在多次与数据库的交互中重用。应用程序操纵脱管对象实例 的状态,这个脱管对象实例最初是在另一个Session 中载入的,然后 调用 Session.update()Session.saveOrUpdate(), 或者 Session.merge() 来重新关联该对象实例。

// foo is an instance loaded by a previous Session
foo.setProperty("bar");
session = factory.openSession();
Transaction t = session.beginTransaction();
session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already
t.commit();
session.close();

Hibernate会再一次在同步的时候检查对象实例的版本,如果发生更新冲突,就抛出异常。

如果你确信对象没有被修改过,你也可以调用lock() 来设置 LockMode.READ(绕过所有的缓存,执行版本检查),从而取 代 update()操作。

12.3.4. 定制自动版本化行为

对于特定的属性和集合,通过为它们设置映射属性optimistic-lock的值 为false,来禁止Hibernate的版本自动增加。这样的话,如果该属性 脏数据,Hibernate将不再增加版本号。

遗留系统的数据库Schema通常是静态的,不可修改的。或者,其他应用程序也可能访问同一数据 库,根本无法得知如何处理版本号,甚至时间戳。在以上的所有场景中,实现版本化不能依靠 数据库表的某个特定列。在<class>的映射中设置 optimistic-lock="all"可以在没有版本或者时间戳属性映射的情况下实现 版本检查,此时Hibernate将比较一行记录的每个字段的状态。请注意,只有当Hibernate能够比 较新旧状态的情况下,这种方式才能生效,也就是说, 你必须使用单个长生命周期Session模式,而不能使用 session-per-request-with-detached-objects模式。

有些情况下,只要更改不发生交错,并发修改也是允许的。当你在<class> 的映射中设置optimistic-lock="dirty",Hibernate在同步的时候将只比较有脏 数据的字段。

在以上所有场景中,不管是专门设置一个版本/时间戳列,还是进行全部字段/脏数据字段比较, Hibernate都会针对每个实体对象发送一条UPDATE(带有相应的 WHERE语句 )的SQL语句来执行版本检查和数据更新。如果你对关联实体 设置级联关系使用传播性持久化(transitive persistence),那么Hibernate可能会执行不必 要的update语句。这通常不是个问题,但是数据库里面对on update点火 的触发器可能在脱管对象没有任何更改的情况下被触发。因此,你可以在 <class>的映射中,通过设置select-before-update="true" 来定制这一行为,强制Hibernate SELECT这个对象实例,从而保证, 在更新记录之前,对象的确是被修改过。

12.4. 悲观锁定(Pessimistic Locking)

用户其实并不需要花很多精力去担心锁定策略的问题。通常情况下,只要为JDBC连接指定一下隔 离级别,然后让数据库去搞定一切就够了。然而,高级用户有时候希望进行一个排它的悲观锁定, 或者在一个新的事务启动的时候,重新进行锁定。

Hibernate总是使用数据库的锁定机制,从不在内存中锁定对象!

LockMode 定义了Hibernate所需的不同的锁定级别。一个锁定 可以通过以下的机制来设置:

  • 当Hibernate更新或者插入一行记录的时候,锁定级别自动设置为LockMode.WRITE

  • 当用户显式的使用数据库支持的SQL格式SELECT ... FOR UPDATE 发送SQL的时候,锁定级别设置为LockMode.UPGRADE

  • 当用户显式的使用Oracle数据库的SQL语句SELECT ... FOR UPDATE NOWAIT 的时候,锁定级别设置LockMode.UPGRADE_NOWAIT

  • 当Hibernate在“可重复读”或者是“序列化”数据库隔离级别下读取数据的时候,锁定模式 自动设置为LockMode.READ。这种模式也可以通过用户显式指定进行设置。

  • LockMode.NONE 代表无需锁定。在Transaction结束时, 所有的对象都切换到该模式上来。与session相关联的对象通过调用update() 或者saveOrUpdate()脱离该模式。

"显式的用户指定"可以通过以下几种方式之一来表示:

  • 调用 Session.load()的时候指定锁定模式(LockMode)

  • 调用Session.lock()

  • 调用Query.setLockMode()

如果在UPGRADE或者UPGRADE_NOWAIT锁定模式下调 用Session.load(),并且要读取的对象尚未被session载入过,那么对象 通过SELECT ... FOR UPDATE这样的SQL语句被载入。如果为一个对象调用 load()方法时,该对象已经在另一个较少限制的锁定模式下被载入了,那 么Hibernate就对该对象调用lock() 方法。

如果指定的锁定模式是READ, UPGRADEUPGRADE_NOWAIT,那么Session.lock()就 执行版本号检查。(在UPGRADE 或者UPGRADE_NOWAIT 锁定模式下,执行SELECT ... FOR UPDATE这样的SQL语句。)

如果数据库不支持用户设置的锁定模式,Hibernate将使用适当的替代模式(而不是扔出异常)。 这一点可以确保应用程序的可移植性。



拦截器与事件(Interceptors and events)

应用程序能够响应Hibernate内部产生的特定事件是非常有用的。这样就允许实现某些通用的功能 以及允许对Hibernate功能进行扩展。

13.1.  拦截器(Interceptors)

Interceptor接口提供了从会话(session)回调(callback)应用程序(application)的机制, 这种回调机制可以允许应用程序在持久化对象被保存、更新、删除或是加载之前,检查并(或)修改其 属性。一个可能的用途,就是用来跟踪审核(auditing)信息。例如:下面的这个拦截器,会在一个实现了 Auditable接口的对象被创建时自动地设置createTimestamp属性,并在实现了 Auditable接口的对象被更新时,同步更新lastUpdateTimestamp属性。

package org.hibernate.test;
import java.io.Serializable;
import java.util.Date;
import java.util.Iterator;
import org.hibernate.Interceptor;
import org.hibernate.type.Type;
public class AuditInterceptor implements Interceptor, Serializable {
private int updates;
private int creates;
public void onDelete(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
// do nothing
}
public boolean onFlushDirty(Object entity,
Serializable id,
Object[] currentState,
Object[] previousState,
String[] propertyNames,
Type[] types) {
if ( entity instanceof Auditable ) {
updates++;
for ( int i=0; i < propertyNames.length; i++ ) {
if ( "lastUpdateTimestamp".equals( propertyNames[i] ) ) {
currentState[i] = new Date();
return true;
}
}
}
return false;
}
public boolean onLoad(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
return false;
}
public boolean onSave(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
if ( entity instanceof Auditable ) {
creates++;
for ( int i=0; i<propertyNames.length; i++ ) {
if ( "createTimestamp".equals( propertyNames[i] ) ) {
state[i] = new Date();
return true;
}
}
}
return false;
}
public void postFlush(Iterator entities) {
System.out.println("Creations: " + creates + ", Updates: " + updates);
}
public void preFlush(Iterator entities) {
updates=0;
creates=0;
}
...
}

创建会话(session)的时候可以指定拦截器。

Session session = sf.openSession( new AuditInterceptor() );

你也可以使用Configuration来设置一个全局范围的拦截器。

new Configuration().setInterceptor( new AuditInterceptor() );

13.2.  事件系统(Event system)

如果需要响应持久层的某些特殊事件,你也可以使用Hibernate3的事件框架。 该事件系统可以用来替代拦截器,也可以作为拦截器的补充来使用。

基本上,Session接口的每个方法都有相对应的事件。比如 LoadEventFlushEvent,等等(查阅XML配置文件 的DTD,以及org.hibernate.event包来获得所有已定义的事件的列表)。当某个方 法被调用时,Hibernate Session会生成一个相对应的事件并激活所 有配置好的事件监听器。系统预设的监听器实现的处理过程就是被监听的方法要做的(被监听的方法所做的其实仅仅是激活监听器, “实际”的工作是由监听器完成的)。不过,你可以自由地选择实现 一个自己定制的监听器(比如,实现并注册用来处理处理LoadEventLoadEventListener接口), 来负责处理所有的调用Sessionload()方法的请求。

监听器应该被看作是单例(singleton)对象,也就是说,所有同类型的事件的处理共享同一个监听器实例,因此监听器 不应该保存任何状态(也就是不应该使用成员变量)。

用户定制的监听器应该实现与所要处理的事件相对应的接口,或者从一个合适的基类继承(甚至是从Hibernate自带的默认事件监听器类继承, 为了方便你这样做,这些类都被声明成non-final的了)。用户定制的监听器可以通过编程使用Configuration对象 来注册,也可以在Hibernate的XML格式的配置文件中进行声明(不支持在Properties格式的配置文件声明监听器)。 下面是一个用户定制的加载事件(load event)的监听器:

public class MyLoadListener extends DefaultLoadEventListener {
// this is the single method defined by the LoadEventListener interface
public Object onLoad(LoadEvent event, LoadEventListener.LoadType loadType)
throws HibernateException {
if ( !MySecurity.isAuthorized( event.getEntityClassName(), event.getEntityId() ) ) {
throw MySecurityException("Unauthorized access");
}
return super.onLoad(event, loadType);
}
}

你还需要修改一处配置,来告诉Hibernate以使用选定的监听器来替代默认的监听器。

<hibernate-configuration>
<session-factory>
...
<listener type="load" class="MyLoadListener"/>
</session-factory>
</hibernate-configuration>

看看用另一种方式,通过编程的方式来注册它。

Configuration cfg = new Configuration();
cfg.getSessionEventListenerConfig().setLoadEventListener( new MyLoadListener() );

通过在XML配置文件声明而注册的监听器不能共享实例。如果在多个<listener/>节点中使用 了相同的类的名字,则每一个引用都将会产生一个独立的实例。如果你需要在多个监听器类型之间共享 监听器的实例,则你必须使用编程的方式来进行注册。

为什么我们实现了特定监听器的接口,在注册的时候还要明确指出我们要注册哪个事件的监听器呢? 这是因为一个类可能实现多个监听器的接口。在注册的时候明确指定要监听的事件,可以让启用或者禁用对某个事件的监听的配置工作简单些。

13.3.  Hibernate的声明式安全机制

通常,Hibernate应用程序的声明式安全机制由会话外观层(session facade)所管理。 现在,Hibernate3允许某些特定的行为由JACC进行许可管理,由JAAS进行授权管理。 本功能是一个建立在事件框架之上的可选的功能。

首先,你必须要配置适当的事件监听器(event listener),来激活使用JAAS管理授权的功能。

<listener type="pre-delete" class="org.hibernate.secure.JACCPreDeleteEventListener"/>
<listener type="pre-update" class="org.hibernate.secure.JACCPreUpdateEventListener"/>
<listener type="pre-insert" class="org.hibernate.secure.JACCPreInsertEventListener"/>
<listener type="pre-load" class="org.hibernate.secure.JACCPreLoadEventListener"/>

接下来,仍然在hibernate.cfg.xml文件中,绑定角色的权限:

<grant role="admin" entity-name="User" actions="insert,update,read"/>
<grant role="su" entity-name="User" actions="*"/>

这些角色的名字就是你的JACC provider所定义的角色的名字。

posted on 2007-05-15 15:09 ☜♥☞MengChuChen 阅读(1930) 评论(0)  编辑  收藏 所属分类: hibernate

只有注册用户登录后才能发表评论。


网站导航: