1. 程式人生 > >spring-data-jpa+hibernate 各種緩存的配置演示

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

mark num rest net posit bstr doc 技術 對象

本文所有測試用代碼在https://github.com/wwlleo0730/restjplat 的分支addDB上

目前在使用spring-data-jpa和hibernate4的時候,對於緩存關系不是很清楚,以及二級緩存和查詢緩存的各種配置等等,於是就有了這篇初級的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.class, 1); //find id為1的對象
  4. Dict d2 = em.find(Dict.class, 1); //find id為1的對象
  5. logger.info((d1==d2)+""); //true
  6. EntityManager em1 = emf.createEntityManager();
  7. Dict d3 = em1.find(Dict.class, 1); //find id為1的對象
  8. EntityManager em2 = emf.createEntityManager();
  9. Dict d4 = em2.find(Dict.class, 1); //find id為1的對象
  10. logger.info((d3==d4)+""); //false
  11. }



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.class, 1); //find id為1的對象
  4. logger.info(d1.getName());
  5. em1.close();
  6. EntityManager em2 = emf.createEntityManager();
  7. Dict d2 = em2.find(Dict.class, 1); //find id為1的對象
  8. logger.info(d2.getName());
  9. em2.close();
  10. }


輸出:

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. // spring-data-jpa默認繼承實現的一些方法,實現類為
  3. // SimpleJpaRepository。
  4. // 該類中的方法不能通過@QueryHint來實現查詢緩存。
  5. @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })
  6. List<Dict> findAll();
  7. @Query("from Dict")
  8. @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })
  9. List<Dict> findAllCached();
  10. @Query("select t from Dict t where t.name = ?1")
  11. @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })
  12. Dict findDictByName(String name);
  13. }



測試方法:

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. private static final long serialVersionUID = 1L;
  6. private String name;
  7. private List<Children> clist;
  8. public String getName() {
  9. return name;
  10. }
  11. public void setName(String name) {
  12. this.name = name;
  13. }
  14. @OneToMany(fetch = FetchType.EAGER,mappedBy = "parent")
  15. @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
  16. public List<Children> getClist() {
  17. return clist;
  18. }
  19. public void setClist(List<Children> clist) {
  20. this.clist = clist;
  21. }
  22. }
  23. @Entity
  24. @Table(name ="children")
  25. @Cacheable
  26. public class Children extends IdEntity{
  27. private static final long serialVersionUID = 1L;
  28. private String name;
  29. private Parent parent;
  30. @ManyToOne(fetch = FetchType.LAZY)
  31. @JoinColumn(name = "parent_id")
  32. public Parent getParent() {
  33. return parent;
  34. }
  35. public void setParent(Parent parent) {
  36. this.parent = parent;
  37. }
  38. public String getName() {
  39. return name;
  40. }
  41. public void setName(String name) {
  42. this.name = name;
  43. }
  44. }



測試方法:

Java代碼 技術分享
  1. private void cellectionCacheTest(){
  2. EntityManager em1 = emf.createEntityManager();
  3. Parent p1 = em1.find(Parent.class, 1);
  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.class, 1);
  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. 然後到 QueryImpl<X>類的
  5. private List<X> list() {
  6. if (getEntityGraphQueryHint() != null) {
  7. SessionImplementor sessionImpl = (SessionImplementor) getEntityManager().getSession();
  8. HQLQueryPlan entityGraphQueryPlan = new HQLQueryPlan( getHibernateQuery().getQueryString(), false,
  9. sessionImpl.getEnabledFilters(), sessionImpl.getFactory(), getEntityGraphQueryHint() );
  10. // Safe to assume QueryImpl at this point.
  11. unwrap( org.hibernate.internal.QueryImpl.class ).setQueryPlan( entityGraphQueryPlan );
  12. }
  13. return query.list();
  14. }
  15. 進入query.list();
  16. query類的代碼解析google一下很多,於是直接到最後:
  17. 進入QueryLoader的list方法。
  18. protected List list(
  19. final SessionImplementor session,
  20. final QueryParameters queryParameters,
  21. final Set<Serializable> querySpaces,
  22. final Type[] resultTypes) throws HibernateException {
  23. final boolean cacheable = factory.getSettings().isQueryCacheEnabled() &&
  24. queryParameters.isCacheable();
  25. if ( cacheable ) {
  26. return listUsingQueryCache( session, queryParameters, querySpaces, resultTypes );
  27. }
  28. else {
  29. return listIgnoreQueryCache( session, queryParameters );
  30. }
  31. }



果然有個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. public List list() throws HibernateException {
  3. verifyParameters();
  4. Map namedParams = getNamedParams();
  5. before();
  6. try {
  7. return getSession().list(
  8. expandParameterLists(namedParams),
  9. getQueryParameters(namedParams)
  10. );
  11. }
  12. finally {
  13. after();
  14. }
  15. }
  16. getQueryParameters(namedParams)這個方法實際獲取的是query對象的cacheable屬性的值,也就是說,query對象新建的時候cacheable的值決定了這個query方法能不能被查詢緩存。



接下來query的建立過程:

Java代碼 技術分享
  1. 在 SimpleJpaRepository 類中 return applyLockMode(em.createQuery(query));
  2. 直接由emcreate,再跟蹤到 AbstractEntityManagerImpl中
  3. @Override
  4. public <T> QueryImpl<T> createQuery(
  5. String jpaqlString,
  6. Class<T> resultClass,
  7. Selection selection,
  8. QueryOptions queryOptions) {
  9. try {
  10. org.hibernate.Query hqlQuery = internalGetSession().createQuery( jpaqlString );
  11. ....
  12. return new QueryImpl<T>( hqlQuery, this, queryOptions.getNamedParameterExplicitTypes() );
  13. }
  14. catch ( RuntimeException e ) {
  15. throw convert( e );
  16. }
  17. }
  18. 即通過session.createQuery(jpaqlString ) 創建初始化對象。
  19. 在query類定義中
  20. public abstract class AbstractQueryImpl implements Query {
  21. private boolean cacheable;
  22. }
  23. 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註解從而設置了查詢緩存。

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