Hibernate.orgCommunity Documentation

第 11 章 与对象共事

11.1. Hibernate 对象状态(object states)
11.2. 使对象持久化
11.3. 装载对象
11.4. 查询
11.4.1. 执行查询
11.4.2. 过滤集合
11.4.3. 条件查询(Criteria queries)
11.4.4. 使用原生 SQL 的查询
11.5. 修改持久对象
11.6. 修改脱管(Detached)对象
11.7. 自动状态检测
11.8. 删除持久对象
11.9. 在两个不同数据库间复制对象
11.10. Session 刷出(flush)
11.11. 传播性持久化(transitive persistence)
11.12. 使用元数据

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

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

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

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

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)将在本章稍后讨论。

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

Cat fritz = (Cat) sess.load(Cat.class, generatedId);
// you need to wrap primitive identifiers

long id = 1234;
DomesticCat pk = (DomesticCat) sess.load( DomesticCat.class, new Long(id) );

此外,你可以把数据(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)

How much does Hibernate load from the database and how many SQL SELECTs will it use? This depends on the fetching strategy. This is explained in 第 21.1 节 “抓取策略(Fetching strategies)”.

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

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();]]
Query mothersWithKittens = (Cat) session.createQuery(
    "select mother from Cat as mother left join fetch mother.kittens");
Set uniqueMothers = new HashSet(mothersWithKittens.list());

一个查询通常在调用 list() 时被执行,执行结果会完全装载进内存中的一个集合(collection)。查询返回的对象处于持久(persistent)状态。如果你知道的查询只会返回一个对象,可使用 list() 的快捷方式 uniqueResult()。注意,使用集合预先抓取的查询往往会返回多次根对象(他们的集合类都被初始化了)。你可以通过一个集合(Set)来过滤这些重复对象。

Queries can also be configured as so called named queries using annotations or Hibernate mapping documents. @NamedQuery and @NamedQueries can be defined at the class level as seen in 例 11.1 “Defining a named query using @NamedQuery” . However their definitions are global to the session factory/entity manager factory scope. A named query is defined by its name and the actual query string.


Using a mapping document can be configured using the <query> node. Remember to use a CDATA section if your query contains characters that could be interpreted as markup.


Parameter binding and executing is done programatically as seen in 例 11.3 “Parameter binding of a named query”.


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

也请注意在 <hibernate-mapping> 元素中声明的查询必须有一个全局唯一的名字,而在 <class> 元素中声明的查询自动具有全局名,是通过类的全名加以限定的。比如 eg.Cat.ByNameAndMaximumWeight

事务中的持久实例(就是通过 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 通过提供 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)对象的操作首先被执行。

The application should individually update() detached instances that are reachable from the given detached instance only if it wants their state to be updated. This can be automated using transitive persistence. See 第 11.11 节 “传播性持久化(transitive persistence)” for more information.

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() 的唯一用途。

Other models for long units of work are discussed in 第 13.3 节 “乐观并发控制(Optimistic concurrency control)”.

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()

saveOrUpdate() 做下面的事:

merge() 可非常不同:

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

sess.delete(cat);

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

偶尔会用到不重新生成持久化标识(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() 如何处理。

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

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

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

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

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

It is possible to change the default behavior so that flush occurs less frequently. The FlushMode class defines three different modes: only flush at commit time when the Hibernate Transaction API is used, flush automatically using the explained routine, or never flush unless flush() is called explicitly. The last mode is useful for long running units of work, where a Session is kept open and disconnected for a long time (see 第 13.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
sess.close();

During flush, an exception might occur (e.g. if a DML operation violates a constraint). Since handling exceptions involves some understanding of Hibernate's transactional behavior, we discuss it in 第 13 章 事务和并发 .

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

如果一个父子关系中的子对象是值类型(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)。

In case you are using annotatons you probably have noticed the cascade attribute taking an array of CascadeType as a value. The cascade concept in JPA is very is similar to the transitive persistence and cascading of operations as described above, but with slightly different semantics and cascading types:

A special cascade style, delete-orphan, applies only to one-to-many associations, and indicates that the delete() operation should be applied to any child object that is removed from the association. Using annotations there is no CascadeType.DELETE-ORPHAN equivalent. Instead you can use the attribute orphanRemoval as seen in 例 11.4 “@OneToMany with orphanRemoval”. If an entity is removed from a @OneToMany collection or an associated entity is dereferenced from a @OneToOne association, this associated entity can be marked for deletion if orphanRemoval is set to true.


建议:

  • It does not usually make sense to enable cascade on a many-to-one or many-to-many association. In fact the @ManyToOne and @ManyToMany don't even offer a orphanRemoval attribute. Cascading is often useful for one-to-one and one-to-many associations.

  • If the child object's lifespan is bounded by the lifespan of the parent object, make it a life cycle object by specifying cascade="all,delete-orphan"(@OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)).

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

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

Furthermore, a mere reference to a child from a persistent parent will result in save/update of the child. This metaphor is incomplete, however. A child which becomes unreferenced by its parent is not automatically deleted, except in the case of a one-to-many association mapped with cascade="delete-orphan". The precise semantics of cascading operations for a parent/child relationship are as follows:

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

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

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

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

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

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

最后,注意操作的级联可能是在调用期(call time)或者写入期(flush time)作用到对象图上的。所有的操作,如果允许,都在操作被执行的时候级联到可触及的关联实体上。然而,save-upatedelete-orphan 是在Session flush 的时候才作用到所有可触及的被关联对象上的。

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] );
    }
}