11.快取
二级快取可以跨越 Session 生命周期,Hibernate 透过第三方来实现二级快取,这边也来看看 Query 的快取。
11.1二级快取(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
<ehcache>
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
</ehcache>
将这个档案放在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
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
....
<property name="hibernate.cache.provider_class">
org.hibernate.cache.HashtableCacheProvider
</property>
....
</session-factory>
</hibernate-configuration>
HashtableCache是Hibernate自己所提供的二级快取实现,不过性能与功能上有限,只用于开发时期的测试之用。
可以在映射文件中指定快取策略,使用<cache>卷标在映像实体或Collection上设定快取策略,例如:
User.hbm.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="onlyfun.caterpillar.User" table="user">
<cache usage="read-only"/>
<id name="id" column="id" type="java.lang.Integer">
<generator class="native"/>
</id>
<property name="name" column="name" type="java.lang.String"/>
<property name="age" column="age" type="java.lang.Integer"/>
</class>
</hibernate-mapping>
可以设定的策略包括read-only、read-write、nonstrict-read-write与transactional,并不是每一个第三方快取实现都支持所有的选项,每一个选项的使用时机与支持的产品,可以直接参考Hibernate官方参考快册的 20.2.The Second Level Cache 。
11.2 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
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
....
<property name="hibernate.cache.use_query_cache">true</property>
....
</session-factory>
</hibernate-configuration>
然后在每次建立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
11.3 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()方法来清除快取。