首页 > 代码库 > Hibernate批量操作(二)

Hibernate批量操作(二)

Hibernate提供了一系列的查询接口,这些接口在实现上又有所不同。这里对Hibernate中的查询接口进行一个小结。

我们首先来看一下session加载实体对象的过程:Session在调用数据库查询前,首先会在缓存中进行查询。如果在内部缓存中通过实体类型和id进行查找并命中,数据状态合法,则直接返回。如果内部缓存中未发现有效数据,则查询第二级缓存,如果第二级缓存命中,则返回。如在第二级缓存中没有命中,则发起数据库查询操作(Select SQL),根据映射配置和Select SQL得到的ResultSet,创建对应的数据对象。将其数据对象纳入当前Session实体管理容器(一级缓存)。执行Interceptor.onLoad方法(如果有对应的Interceptor),将数据对象纳入二级缓存。如果数据对象实现了LifeCycle接口,则调用数据对象的onLoad方法。返回数据对象。

利用缓存可以使查询性能得到了大幅提高,但Hibernate的实现中并非所有的接口都利用了缓存机制。Session2的find/iterator方法均可根据指定条件查询并返回符合查询条件的实体对象集。Hibernate3中的查询接口Query中的list/iterate方法也实现了相同的功能(Hibernate3.x版本中淘汰了find()方法,使用Query查询接口)。从实现机制上,这两个接口没有本质差别。从缓存方面上,find/ list方法通过一条select sql实现查询操作,而iterate则执行1+ N次查询, 首先执行select sql获取满足条件的id,再根据每个id获取对应的记录。

但是,find方法(hibernate2)/Query的list(hibernate3)实际上不利用缓存,它对缓存只写不读。find方法将执行Select从数据库中获得所有符合条件的记录并构造相应的实体对象,实体对象构建完毕之后,就将其纳入缓存中。

而iterate方法(hibernate2)/Query的iterate(hibernate3)则充分利用了缓存中的数据,iterate方法执行时,它首先会执行一条Select SQL以获得所有满足查询条件的数据id,然后再从内部缓存中根据id查找对应的实体对象(类似Session.load方法),如果缓存中存在对应的数据,则直接以此数据对象作为查询结果,如果没有找到,则再执行相应的Select语句从数据库中获得对应的记录,并构建相应的数据对象作为查询结果,并且该结果也被纳入缓存中。这样,如果查询数据只读或者读取相对较为频繁,通过这种机制可以大大减少性能上的损耗。

另一个方面,我们知道内部缓存容量并没有限制,在查询过程中,大量的数据对象被纳入内部缓存中,从而带来了相应的内存消耗。为了控制内部缓存的大小,可以手动清除hibernate的缓存。也可结合iterator方法和evict方法逐条对数据对象进行处理,将内存消耗在可接受的范围之内。如下:

String hql = "from XXX";

Query query = session.createQuery(hql);

Iterator iter = query.iterate();

while(iter.hasNext()){

    Object obj = iter.next();

    session.evict(obj);

}

所以数据量过大时, 应该避免使用find(list),推荐使用iterate(iterate)。实际应用开发中,对于大批量数据处理,还是推荐采用SQL或存储过程实现,以获得较高性能。

 

使用基于游标的数据遍历操作也是一个好的查询方法,通过游标,可以逐条获取数据,从而使得内存处于较为稳定的使用状态。如下:

String hql = "from XXX";

Query query = session.createQuery(hql);

ScrollableResults scres = query.scroll();

while(scRes.next()) {

   Object obj = scRes.get(0);

    //。。。

}

 

此外,Hibernate还提供了一种从Criteria对象中增加查询条件的实现。Criteria Query通过面向对象化的设计,将数据查询条件封装为一个对象。简单来讲,Criteria Query可以看作是传统SQL的对象化表示,如:

Criteria criteria = session.createCriteria(TUser.class);

criteria.add(Expression.eq("name","Erica"));

criteria.add(Expression.eq("sex",new Integer(1)));

criteria.addOrder(Order.asc("name")); //增加排序

这里的criteria 实例实际上是SQL “Select * from t_user where name=’Erica’ and sex=1”的封装。Hibernate 在运行期会根据Criteria 中指定的查询条件(也就是上面代码中通过criteria.add方法添加的查询表达式)生成相应的SQL语句。

 

对于大量数据的查询,可以利用分页来控制每次查询返回记录的数量。在Hibernate中,通过Criteria(或者Query)接口的setFirstResult, setMaxResults 方法来限制一次查询返回的记录范围。下面是一个在Spring模板中使用Query接口分页的示例。

public <T> List<T> findByPage(final String hql, final Object[] values, final int offset, final int pageSize) {

        List<T> list = getHibernateTemplate().executeFind(// 通过一个HibernateCallback对象来执行查询

                new HibernateCallback() {

                    // 实现HibernateCallback接口必须实现的方法

                    public Object doInHibernate(Session session) throws HibernateException, SQLException {

                        // 执行Hibernate分页查询

                        Query query = session.createQuery(hql);

                        if (values != null) {

                            // 为hql语句传入参数

                            for (int i = 0; i < values.length; i++) {

                                query.setParameter(i, values[i]);

                            }

                        }

                        List<T> result = query.setFirstResult(offset).setMaxResults(pageSize).list();

                        return result;

                    }

                });

        return list;

    }

分页通常与排序相关,Hibernate中主要有两种排序方式:1)Sort、2)order-by。

sort :Hibernate中提供了可排序Set,它实现了java.util.SortedSet .  在set元素中可配置sort属性(sort=‘natural‘, 指定采用Java默认排序机制,通过调用数据类型的compareTo方法。可以自定义java.util.Comparator接口的实现实现自定义的排序算法,来作为sort的属性值。Map类型与Set基本一致。但Bag和List不支持sort排序。

order-by:在元素中增加order-by属性(比如order-by="address desc" )可以实现数据库排序. 该特性利用了JDK1.4+ 中的LinkedHashSet以及LinkedHashMap, 由此必须在环境JDK1.4以上才可成功。Set, Map, Bag支持, List不支持该特性.

配置示例:映像文件中设定sort属性,例如若为Set,则如下设定:

<set name="addrs" table="ADDRS" sort="natural">

        <key column="USER_ID"/>

        <element type="string" column="ADDRESS" not-null="true"/>

</set>

如果是Map的话,则如下设定:

<map name="files" table="FILES" sort="natural">

    <key column="USER_ID"/>

    <index column="DESCRIPTION" type="string"/>

    <element type="string" column="FILENAME" not-null="true"/>

</map>

在Set中是这么设定的:

<set name="addrs" table="ADDRS" order-by="ADDRESS desc">

        <key column="USER_ID"/>

        <element type="string" column="ADDRESS" not-null="true"/>

</set>

在Map中也是相同的设定方式,您也可以利用数据库中的函式功能,例如:

<map name="files" table="FILES" order-by="lower(FILENAME)">

    <key column="USER_ID"/>

    <index column="DESCRIPTION" type="string"/>

    <element type="string" column="FILENAME" not-null="true"/>

</map>

 

在查询中,还需要考虑的一个问题: 在设定了1 对多这种关系之后, 查询将会出现n +1 问题。 
1 )1 对多,在1 方查找得到了n个对象, 那么又需要将n 个对象关联的集合取出,于是本来的一条sql查询变成了n +1 条 
2)多对1 ,在多方查询得到了m个对象,那么也会将m个对象对应的1 方的对象取出, 也变成了m+1

解决n+1问题的思路是利用Hibernate提供的两种检索策略:延迟检索策略和迫切左外连接检索策略。延迟检索策略能避免加载应用程序不需要访问的关联对象,迫切左外连接检索策略则充分利用了SQL的外连接查询功能,减少select语句的数目。

?   利用延迟检索策略,则在配置中将lazy设置为true,lazy=true时不会立刻查询关联对象,只有当需要关联对象(访问其属性,非id字段)时才会发生查询动作。使用注释方式,则在本类DTO中有关联外表的表对象的声明,在其的get方法上面加上一个@fetch=fetchtype.lazy。(Hibernate3默认是lazy=true)。

虽然利用延迟检索可以避免执行多余的select语句加载应用程序不需要访问的对象,因此能提高检索性能,并且节省了内存空间;但是,应用程序如果希望访问游离状态代理类实例,必须保证数据对象在持久化状态时已经被初始化。

 

?   利用左外连接检索策略,则在配置中设置outer-join=true,(或采用注解方式:@ManyToOne() @Fetch(FetchMode.JOIN)

左外连接检索策略对应用程序完全透明,不管对象处于持久化状态,还是游离状态,应用程序都可以方便地从一个对象导航到与它关联的对象。同时由于使用了外连接,select语句数目得到了减少;但左连接也可能会加载应用程序不需要访问的对象浪费许多内存空间,并且复杂的数据库表连接也会影响检索性能,不利用进行SQL优化;

对于迫切左外连接检索,query的集合检索并不适用,它会采用立即检索策略,同时,我们还需要通过 hibernate.max_fetch_depth属性来控制外连接的深度,由于外连接使select语句的复杂度提高,多表之间的关联将是很耗时的操作,而且关联越深查询的性能会急速下降。

 

其他的思路还有:利用二级缓存,如果数据在二级缓存中被命中,则不会再引起SQL查询。也可以在关联类上设置@BatchSize(size=2),此时就只发生两条语句。

(@org.hibernate.annotations.BatchSize 允许你定义批量获取该实体的实例数量(如:@BatchSize(size=4)). 当加载一特定的实体时,Hibernate将加载在持久上下文中未经初始化的同类型实体,直至批量数量(上限))
在Criteria接口也可以通过设置setFetchMode来设置检索策略。在网上看到一篇<Hibernate 3如何解决n+1 selects问题>的文章中(如此嵌套没有尝试过),提到嵌套多张外键关联表时,如四张表(one,two,three,four)从one一直外键关联到four,用Session中得到One,并从One里一直取到Four里的内容的做法。代码摘下:

session = sessionFactory.openSession();

Criteria criteria = session.createCriteria(One.class);

criteria.add(Expression.eq("COneId",new Integer(1)));

one = (One)criteria.setFetchMode("twos",FetchMode.JOIN).setFetchMode("twos.threes",FetchMode.JOIN).setFetchMode("twos.threes.fours",FetchMode.JOIN).uniqueResult();

session.close();

在用Criteria之前先设置FetchMode,应为Criteria是动态生成sql语句的,所以生成的sql就是一层层Join下去的。

setFetchMode(String,Mode)第一个参数是association path,用"."来表示路径。这一点具体的例子很少,文档也没有写清楚。我也是试了很久才试出来的。

就这个例子来所把因为取道第四层,所以要进行三次setFetchMode

第一次的路径是twos,一位one中有two的Set。这个具体要更具hbm.xml的配置来定。

第二个路径就是twos.threes

第三个就是twos.threes.fours

一次类推,一层层增加的。

 

此外,还可直接使用SQL来查询,写SQL语句时就写成联合查询的形式。

 

 

 

附:以下参考自http://www.blogjava.net/dreamstone/archive/2007/07/29/133071.html

回顾一下Hibernate实体对象的状态。在Hibernate中,一个对象定义了三种状态:transient(瞬态或者自由态)、persistent(持久化状态)、detached(脱管状态或者游离态)。

    ?   瞬时状态(transient):刚刚用new语句建立,还没有被持久化,不处于session的缓存中,在数据库中无相应记录。处于临时状态的java对象称之为临时对象。
    ?   持久化对象(persistent):已经被持久化,加入到session的缓存中,在数据库中有相应记录。处于持久化状态的java对象被称之为持久化对象,会被session根据持久化对象的属性变化,自动同步更新数据库。
    ?   托管(游离)状态(detached):持久化对象关联的session关闭后处于托管状态,没在Session缓存中,如果没有其他程序删除其对应的纪录,那么数据库中应该有其纪录。可以继续修改然后关联到新的session上,再次成为持久化对象,托管期间的修改会被持久化到DB。这使长时间操作成为可能。

 

这三种状态可以通过Session的一些API调用实现互相转化:

    ?   处于游离状态的实例可以通过调用save()、persist()或者saveOrUpdate()方法进行持久化。
    ?   持久化的实例可以通过调用 delete()变成脱管状态。通过get()或load()方法得到的实例都是持久化状态的。
    ?   脱管状态的实例可以通过调用 update()、saveOrUpdate()、lock()或者replicate()进行持久化。

    ?   save()和persist()将会触发SQL的INSERT操作,delete()会触发SQL DELETE,而update()或merge()会触发SQL UPDATE。对持久化(persistent)实例的修改在刷新提交的时候会被检测到,它也会引起SQL UPDATE。saveOrUpdate()或者replicate()会引发SQL INSERT或者UPDATE

 

这些API的区别如下:

save 和update
        save是把一个新的对象进行保存;update则是对一个脱管状态的对象进行保存。

 

update 和saveOrUpdate
        update()方法操作的对象必须是持久化了的对象,当持久化的对象发生变化时进行保存,如果该对象在数据库中不存在,则会抛出异常。saveOrUpdate()方法操作的对象既可以使持久化了的,也可以使没有持久化的对象。如果是持久化了的对象调用saveOrUpdate()会更新数据库中的对象;如果是未持久化的对象,则save到数据库中,相当于新增了一个对象。

 

persist和save
        参考http://opensource.atlassian.com/projects/hibernate/browse/HHH-1682中的一个说明:

I found that a lot of people have the same doubt. To help to solve this issue 
I‘m quoting Christian Bauer:
"In case anybody finds this thread...

persist() is well defined. It makes a transient instance persistent. However, 
it doesn‘t guarantee that the identifier value will be assigned to the persistent 
instance immediately, the assignment might happen at flush time. The spec doesn‘t say
that, which is the problem I have with persist().

persist() also guarantees that it will not execute an INSERT statement if it is 
called outside of transaction boundaries. This is useful in long-running conversations 
with an extended Session/persistence context.A method like persist() is required.

save() does not guarantee the same, it returns an identifier, and if an INSERT 
has to be executed to get the identifier (e.g. "identity" generator, not "sequence"), 
this INSERT happens immediately, no matter if you are inside or outside of a transaction. This is not good in a long-running conversation with an extended Session/persistence context."

简单翻译一下上边的句子的主要内容:
        1,persist把一个瞬态的实例持久化,但是并"不保证"标识符被立刻填入到持久化实例中,标识符的填入可能被推迟到flush的时间。

        2,persist"保证",当它在一个transaction外部被调用的时候并不触发一个Sql Insert,这个功能是很有用的,当我们通过继承Session/persistence context来封装一个长会话流程的时候,一个persist这样的函数是需要的。

        3,save"不保证"第2条,它要返回标识符,所以它会立即执行Sql insert,不管是不是在transaction内部还是外部

 

saveOrUpdateCopy、merge和update
       merge是用来代替saveOrUpdateCopy的,参考
http://www.blogjava.net/dreamstone/archive/2007/07/28/133053.html
。Merge:将当前对象的状态保存到数据库中,并不会把该对象转换成持久化状态。

      Merge与update的区别在于:当我们使用update的时候,执行完成后,我们提供的对象A的状态变成持久化状态,但当我们使用merge的时候,执行完成,我们提供的对象A还是脱管状态,hibernate或者new了一个B,或者检索到一个持久对象B,并把我们提供的对象A的所有的值拷贝到这个B,执行完成后B是持久状态,而我们提供的A还是托管状态。

 

flush和update
        update操作的是在脱管状态的对象,而flush是操作的在持久状态的对象。
        默认情况下,一个持久状态的对象是不需要update的,只要你更改了对象的值,等待hibernate flush就自动保存到数据库了。hibernate flush发生再几种情况下:
        1,调用某些查询的时候
        2,transaction commit的时候
        3,手动调用flush的时候

 

lock和update
        update是把一个已经更改过的脱管状态的对象变成持久状态;lock是把一个没有更改过的脱管状态的对象变成持久状态。
        对应更改一个记录的内容,两个的操作不同:
        update的操作步骤是:
        (1)更改脱管的对象->调用update
        lock的操作步骤是:
         (2)调用lock把对象从脱管状态变成持久状态-->更改持久状态的对象的内容-->等待flush或者手动flush

 

load和get

区别1:Session.load/get方法均可以根据指定的实体类和id从数据库读取记录,并返回与之对应的实体对象。其区别在于:
如果未能发现符合条件的记录,get方法返回null,而load方法会抛出一个ObjectNotFoundException;load方法可返回实体的代理类实例,而get方法永远直接返回实体类;load方法可以充分利用内部缓存和二级缓存中的现有数据,而get方法则仅仅在内部缓存中进行数据查找,如没有发现对应数据,将越过二级缓存,直接调用SQL完成数据读取。

区别2:load支持延迟加载,get不支持延迟加载。load 在加载的时候会根据加载策略来加载东西,加载策略默认为延迟加载,即只加载id.,如果需要用其它数据,必须在session关闭之前,去加载某一 个属性。lazy="true" or "false" 如果加载策略是立即加载,那么它在加载时会把数据信息全部加载,这个时候即使,关闭session,因为数据已经全部加载了,也能取得数据。get 会直接采用立即加载策略加载数据,不管你配置的是延迟加载还是立即加载。关于立即加载和延迟加载 不仅只对自己这张表,将来表与表之间有关系时,一样会起作用。

无论是get还是load,都会首先查找缓存(一级缓存),如果没有,才会去数据库查找,调用clear()方法,可以强制清除session缓存,调用flush()方法可以强制进行从内存到数据库的同步。

 

Query的list和iterator方法的不同:

list不会使用缓存,而iterate会先取数据库select id出来,然后一个id一个id的load,如果在缓存里面有,就从缓存取,没有的话就去数据库load。

不管是list方法还是iterate方法,第一次查询的时候,它们的查询方式很它们平时的方式是一样的,list执行一条sql,iterate执行1+N条,多出来的行为是它们填充了缓存查询缓存需要打开相关类的class缓存。list和iterate方法第一次执行的时候,都是既填充查询缓存又填充class缓存的。

这里还有一个很容易被忽视的重要问题,即打开查询缓存以后,即使是list方法也可能遇到1+N的问题!

a>list取出所有

b>iterate取出id,等要用的时候再根据id取出对象

c>session中的list第二次发出,仍然会到数据库查询

d>iterate第二次,首先找session级缓存

getCurrentSession()一定要在事务中使用!!!

Hibernate批量操作(二)