对象(实体的实例)对一个特定的Session来说,要么是一个瞬时(transient)对象,要么是持久化(persistent)对象。刚刚创建的对象当然是瞬时的(注:后文中transient object也称为临时对象)。session则提供了把瞬时实例保存(持久化)的服务:
DomesticCat fritz = new DomesticCat(); fritz.setColor(Color.GINGER); fritz.setSex('M'); fritz.setName("Fritz"); Long generatedId = (Long) sess.save(fritz);
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) );
单参数的save()方法为fritz生成了一个惟一标识符,并赋给这个对象。双参数的形式则使用给定的标识符保存pk。我们一般不鼓励使用双参数的形式,因为这可能会(隐含)使主键赋予业务含义。它有用的时候是在一些特殊场合下,比如使用Hibernate来持久化一个BMP实体bean.
关联的对象可以用你喜欢的任何顺序持久化,除非有外键字段具有NOT NULL的约束。决不会有外键约束冲突的危险。然而,如果在save()对象的时候用错了顺序,会触犯NOT NULL约束。
如果你已知某个持久化实例的标识符,Session的load()方法让你取出它。第一种形式使用一个类对象作为参数,会把状态装载到另一个新创建的对象中去。第二个版本允许你给出一个实例,会在其中装载状态。把实例作为参数的形式在你准备把Hibernate和BMP实体bean一起使用的时候特别有用,它就是为此设计的。你也可以发现其他的用途(比如自己实现实例池等等)。
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) );
Cat cat = new DomesticCat(); // load pk's state into cat sess.load( cat, new Long(pkId) ); Set kittens = cat.getKittens();
请注意如果没有匹配的数据库记录,load()方法可能抛出无法恢复的exception。如果类是通过代理映射的,load()方法返回一个对象,这是一个未初始化的代理,并且直到你调用该对象的某方法时才会去访问数据库。这种行为方式在你喜欢创建一个指向某对象的关联,又不想真的从数据库中装载它的时候特别有用。
如果你不确定是否有匹配的行存在,你应该使用get()方法,它会立刻访问数据库,如果没有对应的行,返回null。
Cat cat = (Cat) sess.get(Cat.class, id); if (cat==null) { cat = new Cat(); sess.save(cat, id); } return cat;
你可以用SQLSELECT ... FOR UPDATE装载对象。下一节有关于Hibernate LockMode的讨论。
Cat cat = (Cat) sess.get(Cat.class, id, LockMode.UPGRADE);
注意,任何关联的实例或者包含的集合都不会被做为FOR UPDATE返回。
任何时候都可以使用refresh()方法重新装载对象和它的集合。如果你使用数据库触发器更改了对象的某些属性,这就很有用。
sess.save(cat); sess.flush(); //force the SQL INSERT sess.refresh(cat); //re-read the state (after the trigger executes)
如果你不能确定你要寻找的对象的标示符,请使用Session的find()方法。Hibernate使用一种简单而强大的面向对象查询语言。
List cats = sess.find( "from Cat as cat where cat.birthdate = ?", date, Hibernate.DATE ); List mates = sess.find( "select mate from Cat as cat join cat.mate as mate " + "where cat.name = ?", name, Hibernate.STRING ); List cats = sess.find( "from Cat as cat where cat.mate.bithdate is null" ); List moreCats = sess.find( "from Cat as cat where " + "cat.name = 'Fritz' or cat.id = ? or cat.id = ?", new Object[] { id1, id2 }, new Type[] { Hibernate.LONG, Hibernate.LONG } ); List mates = sess.find( "from Cat as cat where cat.mate = ?", izi, Hibernate.entity(Cat.class) ); List problems = sess.find( "from GoldFish as fish " + "where fish.birthday > fish.deceased or fish.birthday is null" );
find()的第二个参数接受一个对象或者对象数组。第三个参数接受一个Hibernate类型或者类型的数组。这些指定的类型用来把给定的对象绑定到查询中的?占位符(实际上对应的是JDBC PreparedStatement的传入参数)。就像在JDBC中一眼,你应该优先使用这种参数绑定的方式,而非组装字符串。
Hibernate类定义了一些静态方法和常量,提供了访问大部分内置类型的手段。这些内置类型是net.sf.hibernate.type.Type的实例。
如果你知道你的查询会返回非常大量的对象,但是你不希望全部使用它们,你可以用iterate()方法获得更好的性能,它会返回一个java.util.Iterator。这个迭代器会在需要的时候装载对象,所使用的标识符来自一个前导的SQL查询。(一共是N+1次查询)
// fetch ids Iterator iter = sess.iterate("from eg.Qux q order by q.likeliness"); 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; } }
很不幸,java.util.Iterator没有声明任何exception。所以,发生的任何SQL或者Hibernate的exception都会被包装在一个LazyInitializationException中(它是RuntimeException的子类)。
如果你预期大部分的对象已经装载过,存在于session的缓存中了,或者查询结果包含同样的对象很多次,那么iterator()方法也会获得更好的性能。(如果没有任何数据被缓存或者重复出现,则find()总是会更快。)下面是一个应该使用iterator()调用的查询例子:
Iterator iter = sess.iterate( "select customer, product " + "from Customer customer, " + "Product product " + "join customer.purchases purchase " + "where product = purchase.product" );
如果对上面的查询使用find(),会返回一个非常大的JDBCResultSet,包含很多重复的相同数据。
有时候Hibernate查询会每行返回多种对象,这种情况下,每行会返回一个数组,包含多个对象元素:
Iterator foosAndBars = sess.iterate( "select foo, bar from Foo foo, Bar bar " + "where bar.date = foo.date" ); while ( foosAndBars.hasNext() ) { Object[] tuple = (Object[]) foosAndBars.next(); Foo foo = tuple[0]; Bar bar = tuple[1]; .... }
查询可以在select子句中指定类的属性。甚至可以调用SQL的统计函数。属性或者统计值被称为“标量(scalar)”结果。
Iterator results = sess.iterate( "select cat.color, min(cat.birthdate), count(cat) from Cat cat " + "group by cat.color" ); while ( results.hasNext() ) { Object[] row = results.next(); Color type = (Color) row[0]; Date oldest = (Date) row[1]; Integer count = (Integer) row[2]; ..... }
Iterator iter = sess.iterate( "select cat.type, cat.birthdate, cat.name from DomesticCat cat" );
List list = sess.find( "select cat, cat.mate.name from DomesticCat cat" );
如果你需要为你的结果集设置边界(你需要获取的最大行数与/或你希望获取的第一行),你应该得到一个net.sf.hibernate.Query的实例:
Query q = sess.createQuery("from DomesticCat cat"); q.setFirstResult(20); q.setMaxResults(10); List cats = q.list();
你甚至可以在映射文档中定义命名查询。(记得用一个CDATA块把你的查询包含起来,否则在分析的时候可能引起误解。)
<query name="eg.DomesticCat.by.name.and.minimum.weight"><![CDATA[ from eg.DomesticCat as cat where cat.name = ? and cat.weight > ? ] ]></query>
Query q = sess.getNamedQuery("eg.DomesticCat.by.name.and.minimum.weight"); q.setString(0, name); q.setInt(1, minWeight); List cats = q.list();
查询界面支持使用命名参数。命名参数用:name的形式在查询字符串中表示。在Query中有方法把实际参数绑定到命名参数或者JDBC风格的?参数。 和JDBC不同,Hibernate的参数从0开始计数。 使用命名参数有一些好处:
命名参数不依赖于它们在查询字符串中出现的顺序
在同一个查询中可以使用多次
他们可读性好
//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();
如果你的JDBC驱动支持可滚动的ResuleSet,Query接口可以获取一个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) ); }
scroll()的行为方式与iterate()很类似,除了对象可以有选择的用get(int)初始化,而非整个行都一次性被初始化。
集合filter是一种特殊的查询,用于一个持久化集合或者数组。查询字符串可以引用this,意为当前的数组元素。
Collection blackKittens = session.filter( pk.getKittens(), "where this.color = ?", Color.BLACK, Hibernate.enum(Color.class) );
返回的集合被认为是一个包(bag)。
请注意filter并不需要from 子句(当然需要的话它们也可以加上)。Filter不限定返回它们自己的集合元素。
Collection blackKittenMates = session.filter( pk.getKittens(), "select this.mate where this.color = eg.Color.BLACK" );
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();
如果你对类似于SQL的语法不是感觉很舒服的话,用这种方法开始使用Hibernate可能更容易。这种API也比HQL更可扩展。程序可以提供它们自己的Criterion接口的实现。
你可以使用createSQLQuery()方法,用SQL来表达查询。你必须把SQL别名用大括号包围起来。
List cats = session.createSQLQuery( "SELECT {cat.*} FROM CAT AS {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 AS {cat} WHERE ROWNUM<10", "cat", Cat.class ).list()
和Hibernate查询一样,SQL查询也可以包含命名参数或者顺序参数。
持久化实例(就是通过session装载、保存、创建或者查询出的对象)可以被程序操作,所做的任何修改都会在Session清洗(flushed)的时候被持久化(参见后面的“flushing”部分)。所以最直接的更改一个对象的方法就是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提供了另一种方式。
很多程序需要在一个事务中获取对象,然后发送到界面层去操作,用一个新的事务来保存修改。(在高同步访问的环境中使用这种方式,经常使用附带版本的数据来保证事务独立性。)这种方法需要和上一节所描述的略微不同的编程模型。Hibernate支持这种模型,因为它提供了Session.update()方法。
// in the first session Cat cat = (Cat) firstSession.load(Cat.class, catId); Cat potentialMate = new Cat(); firstSession.save(potentialMate); // in a higher tier of the application cat.setMate(potentialMate); // later, in a new session secondSession.update(cat); // update cat secondSession.update(mate); // update mate
如果拥有catId标识符的Cat在试图update它之前已经被secondSession装载了,会抛出一个异常。
对于给定的临时实例,当且仅当 它们触及的其他临时实例需要保存的时候, 应用程序应该对它们分别各自使用update()。(自动管理生命周期的对象(lifecycle object)除外。)
Hibernate用户曾经要求有一个通用的方法,可以为新建的临时实例生成标识符并保存,或者保存已经存在标识符的临时实例的改动。saveOrUpdate()方法就是用来提供这个功能的。
Hibernate通过对象的标识符的值(或version,或timestamp时间戳)来分辨这是一个“新”(未保存过的)实例,还是一个“已存在”(已经保存或者从先前的session中装载的)的实例。id映射中的unsaved-value(或<version>,或 <timestamp>)用来指定哪个值被用于表示“新”实例。
<id name="id" type="long" column="uid" unsaved-value="null"> <generator class="hilo"/> </id>
unsaved-value允许的取值包括:
any - always save 永远保存
none - always update 永远更新
null - 当标识符是空的时候保存(默认情况)
valid identifier value (合法的标识符值)- 当标识符是null或者这个给定的值时保存
undefined - 对于version 或 timestamp来说的默认值。此时使用标识符检查。(原文: the default for version or timestamp, then identifier check is used.参见下文有进一步描述.)
// 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()方法。有一些程序完全不需要使用这些方法。
通常,update()或saveorUpdate()方法在下列情形下使用:
程序在前面的session中装载了对象
对象被传递到UI(界面)层
对该对象进行了一些修改
对象被传递回业务层
应用程序在第二个session中调用update()保存修改
saveOrUpdate()完成了如下工作:
如果对象已经在这个session中持久化过了,什么都不用做
如果对象没有标识值,调用save()来保存它
如果对象的标识值与unsaved-value中的条件匹配,调用save()来保存它
如果对象使用了版本(version或timestamp),那么除非设置unsaved-value="undefined",版本检查会发生在标识符检查之前.
如果这个session中有另外一个对象具有同样的标识符,抛出一个异常
lock()方法是用来让应用程序把一个未修改的对象重新关联到新session的方法。
//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: 使用SELECT ... FOR UPDATE进行版本检查后关联 sess.lock(pk, LockMode.UPGRADE);
使用Session.delete()会把对象的状态从数据库中移除。当然,你的应用程序可能仍然持有一个指向它的引用。所以,最好这样理解:delete()的用途是把一个持久化实例变成临时实例。
sess.delete(cat);
你可以通过传递给delete()一个Hibernate 查询字符串来一次性删除很多对象。
你现在可以用你喜欢的任何顺序删除对象,不用担心外键约束冲突。当然,如果你搞错了顺序,还是有可能引发在外键字段定义的NOT NULL约束冲突。
要保存或者更新一个对象关联图中所有的所有对象,你必须做到:
保证每一个对象都执行save(), saveOrUpdate() 或 update()方法,或者,
在定义关联对象的映射时,使用cascade="all"或cascade="save-update"。
类似的,要删除一个关系图中的所有对象,必须:
对每一个对象都执行delete(),或者
在定义关联对象的映射时,使用cascade="all",cascade="all-delete-orphan"或cascade="delete"。
建议:
如果子对象的生命期是绑定到父对象的生命期的,通过指定cascade="all"可以把它变成一个自动管理生命周期的对象(lifecycle object)。
否则,必须在应用程序代码中明确地执行save()和delete()。如果你想少敲一些代码,可以使用cascade="sve-update",然后只需明确地delete()。
对一种关联(多对一,或者集合)使用cascade="all"映射,就把这种关联标记为一种父/子(parent/child)风格的关系,对父对象进行保存/更新/删除会导致对(所有)子对象的保存/更新/删除。但是这个比喻并不是特别确切。如果父对象解除了对某个子对象的关联,那这个子对象就不会被自动删除了。除非这是一个一对多的关联,并且标明了cascade="all-delete-orphan"(所有-删除-孤儿)。级联操作的精确语义在下面列出:
如果父对象被保存,所有的子对象会被传递到saveOrUpdate()方法去执行
如果父对象被传递到update()或者saveOrUpdate(),所有的子对象会被传递到saveOrUpdate()方法去执行
如果一个临时的子对象被一个持久化的父对象引用了,它会被传递到saveOrUpdate()去执行
如果父对象被删除了,所有的子对象对被传递到delete()方法执行
如果临时的子对象不再被持久化的父对象引用,什么都不会发生(必要时,程序应该明确的删除这个子对象),除非声明了cascade="all-delete-orphan",在这种情况下,成为“孤儿”的子对象会被删除。
每件隔一段时间,Session会执行一些必需的SQL语句来把内存中的对象和JDBC连接中的状态进行同步。这个过程被称为清洗(flush),默认会在下面的时间点执行:
在某些find()或者iterate()调用的时候
在net.sf.hibernate.Transaction.commit()的时候
在Session.flush()的时候
涉及的SQL语句会按照下面的顺序安排:
所有对实体进行插入的语句,其顺序按照对象执行Session.save()的时间顺序
所有对实体进行更新的语句
所有进行集合删除的语句
所有对集合元素进行删除,更新或者插入的语句
所有进行集合插入的语句
所有对实体进行删除的语句,其顺序按照对象执行Session.delete()的时间顺序
(有一个例外时,如果对象使用native方式进行 ID 生成的话,它们一执行save就会被插入。)
除非你明确地发出了flush()指令,关于Session合时会执行这些JDBC调用是完全无法保证的,只能保证它们执行的前后顺序。当然,Hibernate保证,Session.find(..)绝对不会返回已经失效的数据,也不会返回错误数据。
也可以改变默认的设置,来让清洗发生的不那么频繁。FlushMode类定义了三种不同的方式。大部分情况下,它们只由当你在处理“只读”的事务时才会使用,可能会得到一些(不是那么明显的)性能提高。
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); // execute some queries.... sess.find("from Cat as cat left outer join cat.kittens kitten"); //change to izi is not flushed!! .... tx.commit(); //flush occurs
结束一个session包括四个独立的步骤:
清洗session
提交事务
关闭session
处理异常
如果你正在使用TransactionAPI,你就不用担心这个步骤。在事务提交的时候,隐含就会包括这一步。否则,你应该调用Session.flush()来确保你所有的修改都与数据库同步。
如果你正在使用Hibernate 的Transaction API,代码类似这样:
tx.commit(); // flush the Session and commit the transaction
如果你自行管理JDBC事务,你应该手工对JDBC 连接执行commit()。
sess.flush(); sess.connection().commit(); // not necessary for JTA datasource
如果你决定不提交你的更改:
tx.rollback(); // rollback the transaction
或者:
// not necessary for JTA datasource, important otherwise sess.connection().rollback();
如果你回滚了事务,你应该立即关闭和取消当前session,确保Hibernate内部状态的完整性。
调用Session.close()就标志这个session进入了尾声。close()主要的含义就是与这个session相关的JDBC连接会被放弃。
tx.commit(); sess.close();
sess.flush(); sess.connection().commit(); // not necessary for JTA datasource sess.close();
如果你自行管理连接,close()会返回连接的一个引用,你就可以手工把它关闭,或者返回它到连接池去。其他情况下,close()会把它返回到连接池去。
如果Session抛出了一个exception(包括任何SQLException),你应该立刻回滚这个事务,调用Session.close)()来取消这个Session实例。Session中的一些特定方式会确保session不会处于一个不稳定不完整的状态。
建议采用下面的异常处理片断:
Session sess = factory.openSession(); Transaction tx = null; try { tx = sess.beginTransaction(); // do some work ... tx.commit(); } catch (Exception e) { if (tx!=null) tx.rollback(); throw e; } finally { sess.close(); }
如果你是手工管理JDBC事务的,用下面这段:
Session sess = factory.openSession(); try { // do some work ... sess.flush(); sess.connection().commit(); } catch (Exception e) { sess.connection().rollback(); throw e; } finally { sess.close(); }
如果你是从JTA中获得数据源的:
UserTransaction ut = .... ; Session sess = factory.openSession(); try { // do some work ... sess.flush(); } catch (Exception e) { ut.setRollbackOnly(); throw e; } finally { sess.close(); }
Interceptor接口提供从session到你的应用程序的回调方法,让你的程序可以观察和在持久化对象保存/更改/删除或者装载的时候操作它的属性。一种可能的用途是用来监视统计信息。比如,下面的Interceptor会自动在一个Auditable创建的时候设置其createTimestamp,并且当它被更改的时候,设置其lastUpdateTimestamp属性。
package net.sf.hibernate.test; import java.io.Serializable; import java.util.Date; import java.util.Iterator; import net.sf.hibernate.Interceptor; import net.sf.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() );
Hibernate对所有的实体和值类型都需要一个非常丰富的元级别(meta-level)模型。有时候,这个模型对应用程序本身也会非常有用。比如说,应用程序可能使用Hibernate的元数据来实现一种“智能”的深度拷贝算法,来理解哪些对象应该被拷贝(比如,可变的值类型),那些不应该(不可变的值类型和可能的被关联的实体)。
Hibernate通过ClassMetadata接口,CollectionMetadata接口和Type对象树,暴露出元数据。可以通过SessionFactory获取metadata接口的实例。
Cat fritz = ......; Long id = (Long) catMeta.getIdentifier(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 // TODO: what about components? 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] ); } }