本文全部測試用代碼在https://github.com/wwlleo0730/restjplat 的分支addDB上
目前在使用spring-data-jpa和hibernate4的時候,對於緩存關係不是很清楚,以及二級緩存和查詢緩存的各類配置等等,因而就有了這篇初級的jpa+hibernate緩存配置使用的文章。
JPA和hibernate的緩存關係,以及系統demo環境說明
JPA全稱是:Java Persistence API
java
引用
JPA itself is just a specification, not a product; it cannot perform persistence or anything else by itself.
JPA僅僅只是一個規範,而不是產品;使用JPA自己是不能作到持久化的。
因此,JPA只是一系列定義好的持久化操做的接口,在系統中使用時,須要真正的實現者,在這裏,咱們使用Hibernate做爲實現者。因此,仍是用spring-data-jpa+hibernate4+spring3.2來作demo例子說明本文。
JPA規範中定義了不少的緩存類型:一級緩存,二級緩存,對象緩存,數據緩存,等等一系列概念,搞的人糊里糊塗,具體見這裏:
http://en.wikibooks.org/wiki/Java_Persistence/Caching
不過緩存也必需要有實現,由於使用的是hibernate,因此基本只討論hibernate提供的緩存實現。
不少其餘的JPA實現者,好比toplink(EclipseLink),也許還有其餘的各類緩存實現,在此就不說了。
先直接給出全部的demo例子
hibernate實現中只有三種緩存類型:
一級緩存,二級緩存和查詢緩存。
在hibernate的實現概念裏,他把什麼集合緩存之類的統一放到二級緩存裏去了。
1. 一級緩存測試:
文件配置:
git
- <bean id="entityManagerFactory"
- class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
- <property name="dataSource" ref="dataSource" />
- <property name="jpaVendorAdapter" ref="hibernateJpaVendorAdapter" />
- <property name="packagesToScan" value="com.restjplat.quickweb" />
- <property name="jpaProperties">
- <props>
- <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
- <prop key="hibernate.format_sql">true</prop>
- </props>
- </property>
- </bean>
可見沒有添加任何配置項。
github
- private void firstCacheTest(){
- EntityManager em = emf.createEntityManager();
- Dict d1 = em.find(Dict.class, 1);
- Dict d2 = em.find(Dict.class, 1);
- logger.info((d1==d2)+"");
-
- EntityManager em1 = emf.createEntityManager();
- Dict d3 = em1.find(Dict.class, 1);
- EntityManager em2 = emf.createEntityManager();
- Dict d4 = em2.find(Dict.class, 1);
- logger.info((d3==d4)+"");
- }
- 輸出爲:由於sql語句打出來太長,因此用*號代替
- Hibernate: ***********
- 2014-03-17 20:41:44,819 INFO [main] (DictTest.java:76) - true
- Hibernate: ***********
- Hibernate: ***********
- 2014-03-17 20:41:44,869 INFO [main] (DictTest.java:84) - false
因而可知:同一個session內部,一級緩存生效,同一個id的對象只有一個。不一樣session,一級緩存無效。
2. 二級緩存測試:
文件配置:
1:實體類直接打上 javax.persistence.Cacheable 標記。
web
- @Entity
- @Table(name ="dict")
- @Cacheable
- public class Dict extends IdEntity{}
2:配置文件修改,在 jpaProperties 下添加,用ehcache來實現二級緩存,另外由於加入了二級緩存,咱們將hibernate的統計打開來看看究竟是不是被緩存了。
spring
- <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>
- <prop key="javax.persistence.sharedCache.mode">ENABLE_SELECTIVE</prop>
- <prop key="hibernate.generate_statistics">true</prop>
注1:若是在配置文件中加入了
<prop key="javax.persistence.sharedCache.mode">ENABLE_SELECTIVE</prop>,則不須要在實體內配置hibernate的 @cache標記,只要打上JPA的@cacheable標記便可默認開啓該實體的2級緩存。
注2:若是不使用javax.persistence.sharedCache.mode配置,直接在實體內打@cache標記也能夠。
sql
- @Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
- public class Dict extends IdEntity{}
至於 hibernate的 hibernate.cache.use_second_level_cache這個屬性,文檔裏是這麼寫的:
緩存
引用
Can be used to completely disable the second level cache, which is enabled by default for classes which specify a <cache> mapping.
即打上只要有@cache標記,自動開啓。
因此有兩種方法配置開啓二級緩存:
第一種不使用hibernate的@cache標記,直接用@cacheable標記和緩存映射配置項。
第二種用hibernate的@cache標記使用。
另外javax.persistence.sharedCache.mode的其餘配置以下:
The javax.persistence.sharedCache.mode property can be set to one of the following values:
session
- ENABLE_SELECTIVE (Default and recommended value): entities are not cached unless explicitly marked as cacheable.
- DISABLE_SELECTIVE: entities are cached unless explicitly marked as not cacheable.
- NONE: no entity are cached even if marked as cacheable. This option can make sense to disable second-level cache altogether.
- ALL: all entities are always cached even if marked as non cacheable.
若是用all的話,連實體上的@cacheable都不用打,直接默認所有開啓二級緩存
測試代碼:
app
- private void secondCachetest(){
- EntityManager em1 = emf.createEntityManager();
- Dict d1 = em1.find(Dict.class, 1);
- logger.info(d1.getName());
- em1.close();
-
- EntityManager em2 = emf.createEntityManager();
- Dict d2 = em2.find(Dict.class, 1);
- logger.info(d2.getName());
- em2.close();
- }
輸出:
less
- Hibernate: **************
- a
- a
- ===================L2======================
- com.restjplat.quickweb.model.Dict : 1
可見二級緩存生效了,只輸出了一條sql語句,同時監控中也出現了數據。
另外也能夠看看若是是配置成ALL,而且把@cacheable刪掉,輸出以下:
- Hibernate: ************
- a
- a
- ===================L2======================
- com.restjplat.quickweb.model.Children : 0
- com.restjplat.quickweb.model.Dict : 1
- org.hibernate.cache.spi.UpdateTimestampsCache : 0
- org.hibernate.cache.internal.StandardQueryCache : 0
- com.restjplat.quickweb.model.Parent : 0
- =================query cache=================
而且能夠看見,全部的實體類都加入二級緩存中去了
3. 查詢緩存測試:
一,二級緩存都是根據對象id來查找,若是須要加載一個List的時候,就須要用到查詢緩存。
在Spring-data-jpa實現中,也可使用查詢緩存。
文件配置:
在 jpaProperties 下添加,這裏必須明確標出增長查詢緩存。
- <prop key="hibernate.cache.use_query_cache">true</prop>
而後須要在方法內打上@QueryHint來實現查詢緩存,咱們寫幾個方法來測試以下:
- public interface DictDao extends JpaRepository<Dict, Integer>,JpaSpecificationExecutor<Dict>{
-
-
-
-
- @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })
- List<Dict> findAll();
-
- @Query("from Dict")
- @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })
- List<Dict> findAllCached();
-
- @Query("select t from Dict t where t.name = ?1")
- @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })
- Dict findDictByName(String name);
- }
測試方法:
- private void QueryCacheTest(){
-
-
- dao.findAll();
- dao.findAll();
- System.out.println("================test 1 finish======================");
-
-
- dao.findAllCached();
- dao.findAllCached();
- System.out.println("================test 2 finish======================");
-
-
- dao.findDictByName("a");
- dao.findDictByName("a");
- System.out.println("================test 3 finish======================");
- }
輸出結果:
- Hibernate: **************
- Hibernate: **************
- ================test 1 finish======================
- Hibernate: ***********
- ================test 2 finish======================
- Hibernate: ***********
- ================test 3 finish======================
- ===================L2======================
- com.restjplat.quickweb.model.Dict : 5
- org.hibernate.cache.spi.UpdateTimestampsCache : 0
- org.hibernate.cache.internal.StandardQueryCache : 2
- =================query cache=================
- select t from Dict t where t.name = ?1
- select generatedAlias0 from Dict as generatedAlias0
- from Dict
很明顯,查詢緩存生效。可是爲何第一種方法查詢緩存沒法生效,緣由不明,只能後面看看源代碼了。
4.集合緩存測試:
根據hibernate文檔的寫法,這個應該是算在2級緩存裏面。
測試類:
- @Entity
- @Table(name ="parent")
- @Cacheable
- public class Parent extends IdEntity {
-
- private static final long serialVersionUID = 1L;
- private String name;
- private List<Children> clist;
-
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
-
- @OneToMany(fetch = FetchType.EAGER,mappedBy = "parent")
- @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
- public List<Children> getClist() {
- return clist;
- }
- public void setClist(List<Children> clist) {
- this.clist = clist;
- }
- }
-
- @Entity
- @Table(name ="children")
- @Cacheable
- public class Children extends IdEntity{
-
- private static final long serialVersionUID = 1L;
- private String name;
- private Parent parent;
-
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "parent_id")
- public Parent getParent() {
- return parent;
- }
-
- public void setParent(Parent parent) {
- this.parent = parent;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
- }
測試方法:
- private void cellectionCacheTest(){
- EntityManager em1 = emf.createEntityManager();
- Parent p1 = em1.find(Parent.class, 1);
- List<Children> c1 = p1.getClist();
- em1.close();
- System.out.println(p1.getName()+" ");
- for (Children children : c1) {
- System.out.print(children.getName()+",");
- }
- System.out.println();
- EntityManager em2 = emf.createEntityManager();
- Parent p2 = em2.find(Parent.class, 1);
- List<Children> c2 = p2.getClist();
- em2.close();
- System.out.println(p2.getName()+" ");
- for (Children children : c2) {
- System.out.print(children.getName()+",");
- }
- System.out.println();
- }
輸出:
- Hibernate: ********************
- Michael
- kate,Jam,Jason,Brain,
- Michael
- kate,Jam,Jason,Brain,
- ===================L2======================
- com.restjplat.quickweb.model.Children : 4
- com.restjplat.quickweb.model.Dict : 0
- org.hibernate.cache.spi.UpdateTimestampsCache : 0
- com.restjplat.quickweb.model.Parent.clist : 1
- org.hibernate.cache.internal.StandardQueryCache : 0
- com.restjplat.quickweb.model.Parent : 1
- =================query cache=================
在統計數據裏可見二級緩存的對象數量。
本文咱們不討論關於緩存的更新策略,髒數據等等的東西,只是講解配置方式。
接下來是源代碼篇
理清楚各類配置之後,咱們來看一下hibernate和spring-data-jpa的一些緩存實現源代碼。
上面有個遺留問題,爲何spring-data-jpa默認實現的findAll()方法沒法保存到查詢緩存?只能啃源代碼了。
打斷點跟蹤吧
入口方法是spring-data-jpa裏的 SimpleJpaRepository類
- public List<T> findAll() {
- return getQuery(null, (Sort) null).getResultList();
- }
-
- 而後到 QueryImpl<X>類的
- private List<X> list() {
- if (getEntityGraphQueryHint() != null) {
- SessionImplementor sessionImpl = (SessionImplementor) getEntityManager().getSession();
- HQLQueryPlan entityGraphQueryPlan = new HQLQueryPlan( getHibernateQuery().getQueryString(), false,
- sessionImpl.getEnabledFilters(), sessionImpl.getFactory(), getEntityGraphQueryHint() );
-
- unwrap( org.hibernate.internal.QueryImpl.class ).setQueryPlan( entityGraphQueryPlan );
- }
- return query.list();
- }
-
- 進入query.list();
-
- query類的代碼解析google一下不少,因而直接到最後:
-
- 進入QueryLoader的list方法。
-
- protected List list(
- final SessionImplementor session,
- final QueryParameters queryParameters,
- final Set<Serializable> querySpaces,
- final Type[] resultTypes) throws HibernateException {
-
- final boolean cacheable = factory.getSettings().isQueryCacheEnabled() &&
- queryParameters.isCacheable();
-
- if ( cacheable ) {
- return listUsingQueryCache( session, queryParameters, querySpaces, resultTypes );
- }
- else {
- return listIgnoreQueryCache( session, queryParameters );
- }
- }
果真有個cacheable,值爲false,說明的確是沒有從緩存裏取數據。
用自定義的jpa查詢方法測試後發現,這個值爲true。
因而接着看cacheable的取值過程:
- final boolean cacheable = factory.getSettings().isQueryCacheEnabled() &&
- queryParameters.isCacheable();
factory.getSettings().isQueryCacheEnabled() 這個必定是true,由於是在配置文件中打開的。那隻能是queryParameters.isCacheable() 這個的問題了。
- 在query.list()的方法內部:
-
- public List list() throws HibernateException {
- verifyParameters();
- Map namedParams = getNamedParams();
- before();
- try {
- return getSession().list(
- expandParameterLists(namedParams),
- getQueryParameters(namedParams)
- );
- }
- finally {
- after();
- }
- }
-
- getQueryParameters(namedParams)這個方法實際獲取的是query對象的cacheable屬性的值,也就是說,query對象新建的時候cacheable的值決定了這個query方法能不能被查詢緩存。
接下來query的創建過程:
- 在 SimpleJpaRepository 類中 return applyLockMode(em.createQuery(query));
-
- 直接由emcreate,再跟蹤到 AbstractEntityManagerImpl中
-
- @Override
- public <T> QueryImpl<T> createQuery(
- String jpaqlString,
- Class<T> resultClass,
- Selection selection,
- QueryOptions queryOptions) {
- try {
- org.hibernate.Query hqlQuery = internalGetSession().createQuery( jpaqlString );
-
- ....
- return new QueryImpl<T>( hqlQuery, this, queryOptions.getNamedParameterExplicitTypes() );
- }
- catch ( RuntimeException e ) {
- throw convert( e );
- }
- }
- 即經過session.createQuery(jpaqlString ) 建立初始化對象。
-
- 在query類定義中
- public abstract class AbstractQueryImpl implements Query {
-
- private boolean cacheable;
- }
- cacheable不是對象類型,而是基本類型,因此不賦值的狀況下默認爲「false」。
也就是說spring-data-jpa接口提供的簡單快速的各類接口實現全是不能使用查詢緩存的,徹底不知道爲何這麼設計。
接下來看看咱們本身實現的查詢方法實現:
直接找到query方法的setCacheable()方法打斷點,由於確定改變這個值纔能有查詢緩存。
- 因而跟蹤到 SimpleJpaQuery類中
- protected Query createQuery(Object[] values) {
- return applyLockMode(applyHints(doCreateQuery(values), method), method);
- }
在返回query的過程當中經過applyHints()方法讀取了方法上的QueryHint註解從而設置了查詢緩存。