spring-data-jpa+hibernate 各種緩存的配置演示

JPA和hibernate的緩存關係,以及系統demo環境說明 

JPA全稱是:Java Persistence API 

引用
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. 一級緩存測試: 

文件配置: 

Java代碼  收藏代碼
  1. <bean id="entityManagerFactory"  
  2.     class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">  
  3.     <property name="dataSource" ref="dataSource" />  
  4.     <property name="jpaVendorAdapter" ref="hibernateJpaVendorAdapter" />  
  5.     <property name="packagesToScan" value="com.restjplat.quickweb" />  
  6.     <property name="jpaProperties">  
  7.         <props>  
  8.             <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>  
  9.             <prop key="hibernate.format_sql">true</prop>  
  10.         </props>  
  11.     </property>  
  12. </bean>  


可見沒有添加任何配置項。 

Java代碼  收藏代碼
  1. private void firstCacheTest(){    
  2.     EntityManager em = emf.createEntityManager();  
  3.     Dict d1 = em.find(Dict.class1); //find id爲1的對象  
  4.     Dict d2 = em.find(Dict.class1); //find id爲1的對象  
  5.     logger.info((d1==d2)+""); //true  
  6.   
  7.     EntityManager em1 = emf.createEntityManager();  
  8.     Dict d3 = em1.find(Dict.class1); //find id爲1的對象  
  9.     EntityManager em2 = emf.createEntityManager();  
  10.     Dict d4 = em2.find(Dict.class1); //find id爲1的對象  
  11.     logger.info((d3==d4)+""); //false  
  12. }  


Java代碼  收藏代碼
  1. 輸出爲:因爲sql語句打出來太長,所以用*號代替  
  2. Hibernate: ***********  
  3. 2014-03-17 20:41:44,819  INFO [main] (DictTest.java:76) - true  
  4. Hibernate: ***********  
  5. Hibernate: ***********  
  6. 2014-03-17 20:41:44,869  INFO [main] (DictTest.java:84) - false  


由此可見:同一個session內部,一級緩存生效,同一個id的對象只有一個。不同session,一級緩存無效。 

2. 二級緩存測試: 

文件配置: 

1:實體類直接打上 javax.persistence.Cacheable 標記。 
Java代碼  收藏代碼
  1. @Entity  
  2. @Table(name ="dict")  
  3. @Cacheable  
  4. public class Dict extends IdEntity{}  


2:配置文件修改,在 jpaProperties 下添加,用ehcache來實現二級緩存,另外因爲加入了二級緩存,我們將hibernate的統計打開來看看到底是不是被緩存了。 
Java代碼  收藏代碼
  1. <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>  
  2. <prop key="javax.persistence.sharedCache.mode">ENABLE_SELECTIVE</prop>  
  3. <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標記也可以。 

Java代碼  收藏代碼
  1. @Cache(usage = CacheConcurrencyStrategy.READ_ONLY)  
  2. 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: 
  • 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都不用打,直接默認全部開啓二級緩存 


測試代碼: 

Java代碼  收藏代碼
  1. private void secondCachetest(){  
  2.         EntityManager em1 = emf.createEntityManager();  
  3.         Dict d1 = em1.find(Dict.class1); //find id爲1的對象  
  4.         logger.info(d1.getName());  
  5.         em1.close();  
  6.           
  7.         EntityManager em2 = emf.createEntityManager();  
  8.         Dict d2 = em2.find(Dict.class1); //find id爲1的對象  
  9.         logger.info(d2.getName());  
  10.         em2.close();  
  11.     }  

輸出: 
Java代碼  收藏代碼
  1. Hibernate: **************  
  2. a  
  3. a  
  4. ===================L2======================  
  5. com.restjplat.quickweb.model.Dict : 1  


可見二級緩存生效了,只輸出了一條sql語句,同時監控中也出現了數據。 

另外也可以看看如果是配置成ALL,並且把@cacheable刪掉,輸出如下: 

Java代碼  收藏代碼
  1. Hibernate: ************  
  2. a  
  3. a  
  4. ===================L2======================  
  5. com.restjplat.quickweb.model.Children : 0  
  6. com.restjplat.quickweb.model.Dict : 1  
  7. org.hibernate.cache.spi.UpdateTimestampsCache : 0  
  8. org.hibernate.cache.internal.StandardQueryCache : 0  
  9. com.restjplat.quickweb.model.Parent : 0  
  10. =================query cache=================  


並且可以看見,所有的實體類都加入二級緩存中去了 


3. 查詢緩存測試: 

一,二級緩存都是根據對象id來查找,如果需要加載一個List的時候,就需要用到查詢緩存。 

在Spring-data-jpa實現中,也可以使用查詢緩存。 

文件配置: 
在 jpaProperties 下添加,這裏必須明確標出增加查詢緩存。 
Java代碼  收藏代碼
  1. <prop key="hibernate.cache.use_query_cache">true</prop>  


然後需要在方法內打上@QueryHint來實現查詢緩存,我們寫幾個方法來測試如下: 

Java代碼  收藏代碼
  1. public interface DictDao extends JpaRepository<Dict, Integer>,JpaSpecificationExecutor<Dict>{  
  2.   
  3.     // spring-data-jpa默認繼承實現的一些方法,實現類爲  
  4.     // SimpleJpaRepository。  
  5.     // 該類中的方法不能通過@QueryHint來實現查詢緩存。  
  6.     @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })  
  7.     List<Dict> findAll();  
  8.       
  9.     @Query("from Dict")  
  10.     @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })  
  11.     List<Dict> findAllCached();  
  12.       
  13.     @Query("select t from Dict t where t.name = ?1")  
  14.     @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })  
  15.     Dict findDictByName(String name);  
  16. }  


測試方法 
Java代碼  收藏代碼
  1. private void QueryCacheTest(){  
  2.         //無效的spring-data-jpa實現的接口方法  
  3.         //輸出兩條sql語句  
  4.         dao.findAll();  
  5.         dao.findAll();  
  6.         System.out.println("================test 1 finish======================");  
  7.         //自己實現的dao方法可以被查詢緩存  
  8.         //輸出一條sql語句  
  9.         dao.findAllCached();  
  10.         dao.findAllCached();  
  11.         System.out.println("================test 2 finish======================");  
  12.         //自己實現的dao方法可以被查詢緩存  
  13.         //輸出一條sql語句  
  14.         dao.findDictByName("a");  
  15.         dao.findDictByName("a");  
  16.         System.out.println("================test 3 finish======================");  
  17.     }  


輸出結果: 
Java代碼  收藏代碼
  1. Hibernate: **************  
  2. Hibernate: **************  
  3. ================test 1 finish======================  
  4. Hibernate: ***********  
  5. ================test 2 finish======================  
  6. Hibernate: ***********  
  7. ================test 3 finish======================  
  8. ===================L2======================  
  9. com.restjplat.quickweb.model.Dict : 5  
  10. org.hibernate.cache.spi.UpdateTimestampsCache : 0  
  11. org.hibernate.cache.internal.StandardQueryCache : 2  
  12. =================query cache=================  
  13. select t from Dict t where t.name = ?1  
  14. select generatedAlias0 from Dict as generatedAlias0  
  15. from Dict  


很明顯,查詢緩存生效。但是爲什麼第一種方法查詢緩存無法生效,原因不明,只能後面看看源代碼了。 

4.集合緩存測試: 

根據hibernate文檔的寫法,這個應該是算在2級緩存裏面。 

測試類: 
Java代碼  收藏代碼
  1. @Entity  
  2. @Table(name ="parent")  
  3. @Cacheable  
  4. public class Parent extends IdEntity {  
  5.       
  6.     private static final long serialVersionUID = 1L;  
  7.     private String name;  
  8.     private List<Children> clist;  
  9.       
  10.     public String getName() {  
  11.         return name;  
  12.     }  
  13.     public void setName(String name) {  
  14.         this.name = name;  
  15.     }  
  16.       
  17.     @OneToMany(fetch = FetchType.EAGER,mappedBy = "parent")  
  18.         @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)  
  19.     public List<Children> getClist() {  
  20.         return clist;  
  21.     }  
  22.     public void setClist(List<Children> clist) {  
  23.         this.clist = clist;  
  24.     }  
  25. }  
  26.   
  27. @Entity  
  28. @Table(name ="children")  
  29. @Cacheable  
  30. public class Children extends IdEntity{  
  31.   
  32.     private static final long serialVersionUID = 1L;  
  33.     private String name;  
  34.     private Parent parent;  
  35.       
  36.     @ManyToOne(fetch = FetchType.LAZY)  
  37.     @JoinColumn(name = "parent_id")  
  38.     public Parent getParent() {  
  39.         return parent;  
  40.     }  
  41.   
  42.     public void setParent(Parent parent) {  
  43.         this.parent = parent;  
  44.     }  
  45.   
  46.     public String getName() {  
  47.         return name;  
  48.     }  
  49.   
  50.     public void setName(String name) {  
  51.         this.name = name;  
  52.     }     
  53. }  


測試方法: 
Java代碼  收藏代碼
  1. private void cellectionCacheTest(){  
  2.         EntityManager em1 = emf.createEntityManager();  
  3.         Parent p1 = em1.find(Parent.class1);  
  4.         List<Children> c1 = p1.getClist();  
  5.         em1.close();  
  6.         System.out.println(p1.getName()+" ");  
  7.         for (Children children : c1) {  
  8.             System.out.print(children.getName()+",");  
  9.         }  
  10.         System.out.println();  
  11.         EntityManager em2 = emf.createEntityManager();  
  12.         Parent p2 = em2.find(Parent.class1);  
  13.         List<Children> c2 = p2.getClist();  
  14.         em2.close();  
  15.         System.out.println(p2.getName()+" ");  
  16.         for (Children children : c2) {  
  17.             System.out.print(children.getName()+",");  
  18.         }  
  19.         System.out.println();  
  20.     }  


輸出: 
Java代碼  收藏代碼
  1. Hibernate: ********************  
  2. Michael   
  3. kate,Jam,Jason,Brain,  
  4. Michael   
  5. kate,Jam,Jason,Brain,  
  6. ===================L2======================  
  7. com.restjplat.quickweb.model.Children : 4  
  8. com.restjplat.quickweb.model.Dict : 0  
  9. org.hibernate.cache.spi.UpdateTimestampsCache : 0  
  10. com.restjplat.quickweb.model.Parent.clist : 1  
  11. org.hibernate.cache.internal.StandardQueryCache : 0  
  12. com.restjplat.quickweb.model.Parent : 1  
  13. =================query cache=================  


在統計數據裏可見二級緩存的對象數量。 

本文我們不討論關於緩存的更新策略,髒數據等等的東西,只是講解配置方式。 


接下來是源代碼篇 

理清楚各種配置以後,我們來看一下hibernate和spring-data-jpa的一些緩存實現源代碼。 

上面有個遺留問題,爲什麼spring-data-jpa默認實現的findAll()方法無法保存到查詢緩存?只能啃源代碼了。 

打斷點跟蹤吧 

入口方法是spring-data-jpa裏的 SimpleJpaRepository類 

Java代碼  收藏代碼
  1. public List<T> findAll() {  
  2.         return getQuery(null, (Sort) null).getResultList();  
  3.     }  
  4.   
  5. 然後到 QueryImpl<X>類的  
  6. private List<X> list() {  
  7.         if (getEntityGraphQueryHint() != null) {  
  8.             SessionImplementor sessionImpl = (SessionImplementor) getEntityManager().getSession();  
  9.             HQLQueryPlan entityGraphQueryPlan = new HQLQueryPlan( getHibernateQuery().getQueryString(), false,  
  10.                     sessionImpl.getEnabledFilters(), sessionImpl.getFactory(), getEntityGraphQueryHint() );  
  11.             // Safe to assume QueryImpl at this point.  
  12.             unwrap( org.hibernate.internal.QueryImpl.class ).setQueryPlan( entityGraphQueryPlan );  
  13.         }  
  14.         return query.list();  
  15.     }  
  16.   
  17. 進入query.list();  
  18.   
  19. query類的代碼解析google一下很多,於是直接到最後:  
  20.   
  21. 進入QueryLoader的list方法。  
  22.   
  23. protected List list(  
  24.             final SessionImplementor session,  
  25.             final QueryParameters queryParameters,  
  26.             final Set<Serializable> querySpaces,  
  27.             final Type[] resultTypes) throws HibernateException {  
  28.   
  29.         final boolean cacheable = factory.getSettings().isQueryCacheEnabled() &&  
  30.             queryParameters.isCacheable();  
  31.   
  32.         if ( cacheable ) {  
  33.             return listUsingQueryCache( session, queryParameters, querySpaces, resultTypes );  
  34.         }  
  35.         else {  
  36.             return listIgnoreQueryCache( session, queryParameters );  
  37.         }  
  38.     }  


果然有個cacheable,值爲false,說明的確是沒有從緩存裏取數據。 

用自定義的jpa查詢方法測試後發現,這個值爲true。 

於是接着看cacheable的取值過程: 

Java代碼  收藏代碼
  1. final boolean cacheable = factory.getSettings().isQueryCacheEnabled() &&  
  2.             queryParameters.isCacheable();  


factory.getSettings().isQueryCacheEnabled() 這個一定是true,因爲是在配置文件中打開的。那隻能是queryParameters.isCacheable() 這個的問題了。 

Java代碼  收藏代碼
  1. 在query.list()的方法內部:  
  2.   
  3. public List list() throws HibernateException {  
  4.         verifyParameters();  
  5.         Map namedParams = getNamedParams();  
  6.         before();  
  7.         try {  
  8.             return getSession().list(  
  9.                     expandParameterLists(namedParams),  
  10.                     getQueryParameters(namedParams)  
  11.                 );  
  12.         }  
  13.         finally {  
  14.             after();  
  15.         }  
  16.     }  
  17.   
  18. getQueryParameters(namedParams)這個方法實際獲取的是query對象的cacheable屬性的值,也就是說,query對象新建的時候cacheable的值決定了這個query方法能不能被查詢緩存。  


接下來query的建立過程: 

Java代碼  收藏代碼
  1. 在 SimpleJpaRepository 類中 return applyLockMode(em.createQuery(query));  
  2.   
  3. 直接由emcreate,再跟蹤到 AbstractEntityManagerImpl中  
  4.   
  5. @Override  
  6.     public <T> QueryImpl<T> createQuery(  
  7.             String jpaqlString,  
  8.             Class<T> resultClass,  
  9.             Selection selection,  
  10.             QueryOptions queryOptions) {  
  11.         try {  
  12.             org.hibernate.Query hqlQuery = internalGetSession().createQuery( jpaqlString );  
  13.   
  14.             ....  
  15.             return new QueryImpl<T>( hqlQuery, this, queryOptions.getNamedParameterExplicitTypes() );  
  16.         }  
  17.         catch ( RuntimeException e ) {  
  18.             throw convert( e );  
  19.         }  
  20.     }  
  21. 即通過session.createQuery(jpaqlString ) 創建初始化對象。  
  22.   
  23. 在query類定義中  
  24. public abstract class AbstractQueryImpl implements Query {  
  25.   
  26.         private boolean cacheable;  
  27. }  
  28. cacheable不是對象類型,而是基本類型,所以不賦值的情況下默認爲“false”。  


也就是說spring-data-jpa接口提供的簡單快速的各種接口實現全是不能使用查詢緩存的,完全不知道爲什麼這麼設計。 

接下來看看我們自己實現的查詢方法實現: 

直接找到query方法的setCacheable()方法打斷點,因爲肯定改變這個值纔能有查詢緩存。 


Java代碼  收藏代碼
  1. 於是跟蹤到 SimpleJpaQuery類中  
  2. protected Query createQuery(Object[] values) {  
  3.         return applyLockMode(applyHints(doCreateQuery(values), method), method);  
  4. }  


在返回query的過程中通過applyHints()方法讀取了方法上的QueryHint註解從而設置了查詢緩存。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章