hibernate 二级缓存(三)
过这篇文章纪录hibernate二级缓存的一些使用经历,利用几个test case,从代码角度说明二级缓存在使用过程中一些需要注意的问题使用到的Model类有两个,Author, Book, 两者之间为一对多的关系
@Entity @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Author { private Long id; private String name; private Set<Book> books = new HashSet<Book>(); // getter setter methods omitted }
@Entity @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Author { private Long id; private String name; private Set<Book> books = new HashSet<Book>(); // getter setter methods omitted }
@Entity @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Book { private Long id; private String title; private Author author; // getter setter methods omitted }
@Entity @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Book { private Long id; private String title; private Author author; // getter setter methods omitted }
主要的测试类为TestHibernateSecondLevelCache.java
public class TestHibernateSecondLevelCache { protected Logger logger = LoggerFactory.getLogger(getClass()); private static SessionFactory sessionFactory; @BeforeClass public static void setUpSessionFactory(){ sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory(); } @After public void clearSecondLevelCache(){ logger.info("clear second level cache"); sessionFactory.evict(Author.class); sessionFactory.evict(Book.class); sessionFactory.getStatistics().clear(); } private Session openSession(){ return sessionFactory.openSession(); } private Statistics getStatistics(){ return sessionFactory.getStatistics(); } }
public class TestHibernateSecondLevelCache { protected Logger logger = LoggerFactory.getLogger(getClass()); private static SessionFactory sessionFactory; @BeforeClass public static void setUpSessionFactory(){ sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory(); } @After public void clearSecondLevelCache(){ logger.info("clear second level cache"); sessionFactory.evict(Author.class); sessionFactory.evict(Book.class); sessionFactory.getStatistics().clear(); } private Session openSession(){ return sessionFactory.openSession(); } private Statistics getStatistics(){ return sessionFactory.getStatistics(); } }
方法setUpSessionFactory用于创建HibernateSessionFactory,因为创建SessionFactory是个相对比较耗时的操作,因此加上Junit4的@BeforeClassannotation,表示该SessionFactory只会创建一次,被所有的testcase共享.而clearSecondLevelCache方法会在每个testcase结束时调用,用于清空二级缓存,防止前一个testcase的結果影响后一个testcase
测试使用的hibernate-core版本为:3.3.2.GA,hibernate-annotations版本为:3.4.0.GA,测试的数据库为hsqldb内存数据库
一.session.get()
先来看一下session.get是否会查找二级缓存@Test public void testSessionGetCache(){ Author author = createAuthor(); assertGetMissCache(Author.class, author.getId()); assertGetHitCache(Author.class, author.getId()); updateAuthor(author); assertGetMissCache(Author.class, author.getId()); } private Author createAuthor(){ Session session = openSession(); Author author = new Author(); author.setName("septem"); session.save(author); session.close(); return author; } @SuppressWarnings("unchecked") private void assertGetMissCache(Class clazz, Serializable id){ Statistics stat = getStatistics(); long missCount = stat.getSecondLevelCacheMissCount(); Session session = openSession(); session.get(clazz, id); session.close(); assertEquals(missCount + 1, stat.getSecondLevelCacheMissCount()); } @SuppressWarnings("unchecked") private void assertGetHitCache(Class clazz, Serializable id){ Statistics stat = getStatistics(); long hitCount = stat.getSecondLevelCacheHitCount(); Session session = openSession(); session.get(clazz, id); session.close(); assertEquals(hitCount + 1, stat.getSecondLevelCacheHitCount()); } private void updateAuthor(Author author){ author.setName("new_name"); Session session = openSession(); session.update(author); session.flush(); session.close(); }
@Test public void testSessionGetCache(){ Author author = createAuthor(); assertGetMissCache(Author.class, author.getId()); assertGetHitCache(Author.class, author.getId()); updateAuthor(author); assertGetMissCache(Author.class, author.getId()); } private Author createAuthor(){ Session session = openSession(); Author author = new Author(); author.setName("septem"); session.save(author); session.close(); return author; } @SuppressWarnings("unchecked") private void assertGetMissCache(Class clazz, Serializable id){ Statistics stat = getStatistics(); long missCount = stat.getSecondLevelCacheMissCount(); Session session = openSession(); session.get(clazz, id); session.close(); assertEquals(missCount + 1, stat.getSecondLevelCacheMissCount()); } @SuppressWarnings("unchecked") private void assertGetHitCache(Class clazz, Serializable id){ Statistics stat = getStatistics(); long hitCount = stat.getSecondLevelCacheHitCount(); Session session = openSession(); session.get(clazz, id); session.close(); assertEquals(hitCount + 1, stat.getSecondLevelCacheHitCount()); } private void updateAuthor(Author author){ author.setName("new_name"); Session session = openSession(); session.update(author); session.flush(); session.close(); }
testSessionGetCache首先通过createAuthor创建一个author对象,然后在assertGetMissCache里面通过author.id使用get方法查出之前创建的author,因为这是每一次调用get方法,所以hibernate从数据库取回author对象,并将它存入二级缓存.测试結果通过hibernatestatistics统计信息里的secondlevelcachemisscount来判断这次的get查询未命中缓存
接着assertGetHitCache用同一个id通过get方法获取author对象,因为这个id的对象之前已存入二级缓存,所以这次操作命中缓存
最后通过updateAuthor更新之前的author对象,hibernate会自动将该对象从二级缓存中清除,因此第三次调用get方法时没有命中缓存
总结:session.get方法会先中二级缓存中通过id做为key查找相应的对象,如果不存在,再发送SQL语句到数据库中查询
二.session.load()
第二步试一下session.load方法@Test public void testSessionLoadCache(){ Author author = createAuthor(); assertLoadMissCache(Author.class, author.getId()); assertLoadHitCache(Author.class, author.getId()); updateAuthor(author); assertLoadMissCache(Author.class, author.getId()); } @SuppressWarnings("unchecked") private void assertLoadMissCache(Class clazz, Serializable id){ Statistics stat = getStatistics(); long missCount = stat.getSecondLevelCacheMissCount(); Session session = openSession(); Author author = (Author) session.load(clazz, id); author.getName(); session.close(); assertEquals(missCount + 1, stat.getSecondLevelCacheMissCount()); } @SuppressWarnings("unchecked") private void assertLoadHitCache(Class clazz, Serializable id){ Statistics stat = getStatistics(); long hitCount = stat.getSecondLevelCacheHitCount(); Session session = openSession(); session.load(clazz, id); Author author = (Author) session.load(clazz, id); author.getName(); session.close(); assertEquals(hitCount + 1, stat.getSecondLevelCacheHitCount()); }
@Test public void testSessionLoadCache(){ Author author = createAuthor(); assertLoadMissCache(Author.class, author.getId()); assertLoadHitCache(Author.class, author.getId()); updateAuthor(author); assertLoadMissCache(Author.class, author.getId()); } @SuppressWarnings("unchecked") private void assertLoadMissCache(Class clazz, Serializable id){ Statistics stat = getStatistics(); long missCount = stat.getSecondLevelCacheMissCount(); Session session = openSession(); Author author = (Author) session.load(clazz, id); author.getName(); session.close(); assertEquals(missCount + 1, stat.getSecondLevelCacheMissCount()); } @SuppressWarnings("unchecked") private void assertLoadHitCache(Class clazz, Serializable id){ Statistics stat = getStatistics(); long hitCount = stat.getSecondLevelCacheHitCount(); Session session = openSession(); session.load(clazz, id); Author author = (Author) session.load(clazz, id); author.getName(); session.close(); assertEquals(hitCount + 1, stat.getSecondLevelCacheHitCount()); }
同样的結果,每一次通过idload未命中缓存,第二次通过相同的id调用load方法命中缓存,而更新过author对象后,缓存失效,第三次查询通过数据库获取author
有一点跟get方法不同:Author author = (Author) session.load(clazz, id); author.getName();
Author author = (Author) session.load(clazz, id); author.getName();
总结:调用load方法的时候,hibernate一开始并没有查询二级缓存或是数据库,而是先返回一个代理对象,该对象只包含id,只有显示调用对象的非id属性时,比如author.getName(),hibernate才会去二级缓存查找,如果没命中缓存再去数据库找,数据库还找不到则抛异常.load方法会尽量推迟对象的查找工作,这是它跟get方法最大的区别.
这两者的测试用例如下:@Test(expected=ObjectNotFoundException.class) public void testSessionLoadNonexistAuthor(){ Session session = openSession(); Author author = (Author) session.load(Author.class, -1L); assertEquals(Long.valueOf(-1), author.getId()); author.getName(); session.close(); } @Test public void testSessionGetNonexistAuthor(){ Session session = openSession(); Author author = (Author) session.get(Author.class, -1L); session.close(); assertNull(author); }
@Test(expected=ObjectNotFoundException.class) public void testSessionLoadNonexistAuthor(){ Session session = openSession(); Author author = (Author) session.load(Author.class, -1L); assertEquals(Long.valueOf(-1), author.getId()); author.getName(); session.close(); } @Test public void testSessionGetNonexistAuthor(){ Session session = openSession(); Author author = (Author) session.get(Author.class, -1L); session.close(); assertNull(author); }
三. session.createQuery().list()
@SuppressWarnings("unchecked") @Test public void testSessionList(){ Author author = createAuthor(); createAuthor(); Session session = openSession(); //hit database to select authors and populate the cache List<Author> authors = session.createQuery("from Author").list(); session.close(); assertEquals(authors.size(), getStatistics().getSecondLevelCachePutCount()); Session session2 = openSession(); //hit database again to select authors session2.createQuery("from Author").list(); session2.close(); assertEquals(authors.size(), getStatistics().getSecondLevelCachePutCount()); assertGetHitCache(Author.class, author.getId()); }
@SuppressWarnings("unchecked") @Test public void testSessionList(){ Author author = createAuthor(); createAuthor(); Session session = openSession(); //hit database to select authors and populate the cache List<Author> authors = session.createQuery("from Author").list(); session.close(); assertEquals(authors.size(), getStatistics().getSecondLevelCachePutCount()); Session session2 = openSession(); //hit database again to select authors session2.createQuery("from Author").list(); session2.close(); assertEquals(authors.size(), getStatistics().getSecondLevelCachePutCount()); assertGetHitCache(Author.class, author.getId()); }
首先创建2个author对象,使用HQL : "from Author"调用list方法,这时hibernate直接从数据库查询所有的author对象,并没有从缓存中查询,但是通过list方法查出的所有 author对象会存入二级缓存,这点通过getStatistics().getSecondLevelCachePutCount()可以看出来
接着再调用list方法一次,因为此时还没找开查询缓存,list方法重新从数据查了一次.因为第一次查询已将所有的author存入缓存,所以再调用get方法时会命中缓存,assertGetHitCache通过
总结:list方法不会从二级缓存中查找,但它从数据库中查找出来的对象会被存入cache
四. session.createQuery().iterate()@SuppressWarnings("unchecked") @Test public void testSessionIterate(){ Author author = createAuthor(); createAuthor(); int authorCount = 0; Session session = openSession(); //hit database to get ids for all author Iterator<Author> it = session.createQuery("from Author").iterate(); while(it.hasNext()){ Author a = it.next(); a.getName(); authorCount++; } session.close(); assertEquals(authorCount, getStatistics().getEntityLoadCount()); assertGetHitCache(Author.class, author.getId()); }
@SuppressWarnings("unchecked") @Test public void testSessionIterate(){ Author author = createAuthor(); createAuthor(); int authorCount = 0; Session session = openSession(); //hit database to get ids for all author Iterator<Author> it = session.createQuery("from Author").iterate(); while(it.hasNext()){ Author a = it.next(); a.getName(); authorCount++; } session.close(); assertEquals(authorCount, getStatistics().getEntityLoadCount()); assertGetHitCache(Author.class, author.getId()); }
先创建2个author对象, 通过HQL: "from Author"调用iterate方法,此时hibernate并没有查author对象,而是先从数据库查出所有author的id,控制台会输入类似以下的SQL:
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public Set<Book> getBooks() { return books; }
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public Set<Book> getBooks() { return books; }
测试用例如下:
@Test public void testAssociationCache(){ Author author = createAuthorWith3Books(); assertGetBooksForAuthorMissCache(author, 1); assertGetBooksForAuthorHitCache(author, 4); updateOneBookForAuthor(author); assertGetBooksForAuthorMissCache(author, 1); addNewBookForAuthor(author); assertGetBooksForAuthorMissCache(author, 1); } private Author createAuthorWith3Books(){ Session session = openSession(); Author author = new Author(); author.setName("septem"); Book book1 = new Book(); book1.setTitle("book1"); book1.setAuthor(author); Book book2 = new Book(); book2.setTitle("book2"); book2.setAuthor(author); Book book3 = new Book(); book3.setTitle("book3"); book3.setAuthor(author); author.getBooks().add(book1); author.getBooks().add(book2); author.getBooks().add(book3); session.save(book1); session.save(book2); session.save(book3); session.close(); return author; } private void assertGetBooksForAuthorMissCache(Author author, long miss){ Session session = openSession(); Author a = (Author) session.get(Author.class, author.getId()); long missCount = getStatistics().getSecondLevelCacheMissCount(); a.getBooks().size(); session.close(); assertEquals(missCount + miss, getStatistics().getSecondLevelCacheMissCount()); } private void assertGetBooksForAuthorHitCache(Author author, long hit){ Session session = openSession(); Author a = (Author) session.get(Author.class, author.getId()); long hitCount = getStatistics().getSecondLevelCacheHitCount(); a.getBooks().size(); session.close(); assertEquals(hitCount + hit, getStatistics().getSecondLevelCacheHitCount()); } private void updateOneBookForAuthor(Author author){ Session session = openSession(); Author a = (Author) session.get(Author.class, author.getId()); Book book = (Book) session.get(Book.class, a.getBooks().iterator().next().getId()); book.setTitle("new_title"); session.flush(); session.close(); } private void addNewBookForAuthor(Author author){ Session session = openSession(); Author a = (Author) session.get(Author.class, author.getId()); Book book = new Book(); book.setTitle("new_book"); book.setAuthor(a); a.getBooks().add(book); session.save(book); session.update(a); session.flush(); session.close(); }
@Test public void testAssociationCache(){ Author author = createAuthorWith3Books(); assertGetBooksForAuthorMissCache(author, 1); assertGetBooksForAuthorHitCache(author, 4); updateOneBookForAuthor(author); assertGetBooksForAuthorMissCache(author, 1); addNewBookForAuthor(author); assertGetBooksForAuthorMissCache(author, 1); } private Author createAuthorWith3Books(){ Session session = openSession(); Author author = new Author(); author.setName("septem"); Book book1 = new Book(); book1.setTitle("book1"); book1.setAuthor(author); Book book2 = new Book(); book2.setTitle("book2"); book2.setAuthor(author); Book book3 = new Book(); book3.setTitle("book3"); book3.setAuthor(author); author.getBooks().add(book1); author.getBooks().add(book2); author.getBooks().add(book3); session.save(book1); session.save(book2); session.save(book3); session.close(); return author; } private void assertGetBooksForAuthorMissCache(Author author, long miss){ Session session = openSession(); Author a = (Author) session.get(Author.class, author.getId()); long missCount = getStatistics().getSecondLevelCacheMissCount(); a.getBooks().size(); session.close(); assertEquals(missCount + miss, getStatistics().getSecondLevelCacheMissCount()); } private void assertGetBooksForAuthorHitCache(Author author, long hit){ Session session = openSession(); Author a = (Author) session.get(Author.class, author.getId()); long hitCount = getStatistics().getSecondLevelCacheHitCount(); a.getBooks().size(); session.close(); assertEquals(hitCount + hit, getStatistics().getSecondLevelCacheHitCount()); } private void updateOneBookForAuthor(Author author){ Session session = openSession(); Author a = (Author) session.get(Author.class, author.getId()); Book book = (Book) session.get(Book.class, a.getBooks().iterator().next().getId()); book.setTitle("new_title"); session.flush(); session.close(); } private void addNewBookForAuthor(Author author){ Session session = openSession(); Author a = (Author) session.get(Author.class, author.getId()); Book book = new Book(); book.setTitle("new_book"); book.setAuthor(a); a.getBooks().add(book); session.save(book); session.update(a); session.flush(); session.close(); }
先创建一个author,为该author添加3个book对象.在assertGetBooksForAuthorMissCache通过 author.getBooks访问关联的book集合,因为延迟加载的关系,此时并没有查询缓存也没有查询数据库,在调用 a.getBooks().size()也就是访问book集合的元素时,hibernate先中缓存中查找,没有发现关联缓存,重新数据库查询,生成的 SQL类似如下:
*---------------------------------* | Author Data Cache | |---------------------------------| | 1 -> [ "septem" , [ 1, 2, 3 ] ] | *---------------------------------*
*---------------------------------* | Author Data Cache | |---------------------------------| | 1 -> [ "septem" , [ 1, 2, 3 ] ] | *---------------------------------*
第二步执行assertGetBooksForAuthorHitCache(author, 4)的时候,我们看到hitCount增加了4.因为第二次调用author.getBooks的时候,命中了关联缓存,从缓存中取回3个id,又分别用 id一个一个地从二级缓存中取回3个book对象,一共命中缓存4次
接着通过updateOneBookForAuthor(author)更新了其中的一个book对象,假设更新的是id为1的book.接着的assertGetBooksForAuthorMissCache(author,1)方法里面missCount又增加了1.book虽然更新了,但是author.getBooks还是能命中缓存,因为bookid列表还是[1,2,3].从缓存中取回bookid列表,通过bookid查找book的时候,因为id为1的book已经更新过了,它的二级缓存失效了,重新去数据库取,此时missCount增加了1,而id为2,3的book还是从二级缓存中找到的.这个方法hibernate会生成类似如下的SQL:
@Test public void testQueryCache(){ createAuthor(); createAuthor(); assertQueryMissCache(); assertQueryHitCache(); createAuthor(); assertQueryMissCache(); }
@Test public void testQueryCache(){ createAuthor(); createAuthor(); assertQueryMissCache(); assertQueryHitCache(); createAuthor(); assertQueryMissCache(); }
先做准备工作,创建两个author对象,假设它们的id分别为1,2.assertQueryMissCache里面第一次调用list方法, 注意调用list前必须setCacheable(true)才会使用查询缓存,此时未命中查询缓存,hibernate从数据库查询Author对象, 将此次查询存入查询缓存,同时会将查询到的author对象存入二级缓存
查询缓存并不保存查询结果集,而只是保存结果集的id,它的结构类似以下数据:
*---------------------------------------------------------------* | Query Cache | |---------------------------------------------------------------| | [ ["from Author where name = ?", [ "septem"] ] -> [ 1, 2 ] ] | *---------------------------------------------------------------*
*---------------------------------------------------------------* | Query Cache | |---------------------------------------------------------------| | [ ["from Author where name = ?", [ "septem"] ] -> [ 1, 2 ] ] | *---------------------------------------------------------------*
注意缓存的key与HQL,参数以及分页参数有关
再调用assertQueryHitCache()用同样的HQL与参数重新查询Author此时会命中查询缓存,并根据结果集id一个一个地查询author对象,因为author对象之前已存入二级缓存,所以这次查询也会命中二级缓存
查询缓存的失效比较特殊,只要查询涉及的任何一张表的数据发生变化,缓存就会失效.比如我们再创建一个Author对象,此时Author表发生了变化,原来的查询缓存就失效了
总结: 查询缓存的key与HQL,查询参数以及分布参数有关,而且一旦查询涉及到的任何一张表的数据发生了变化,缓存就失效了,所以在生产环境中命中率较低.查询缓存保存的是结果集的id列表,而不是结果集本身,命中缓存的时候,会根据id一个一个地先从二级缓存查找,找不到再查询数据库.涉及到的所有代码保存在google code上
svncheckouthttp://hibernate-cache-testcase.googlecode.com/svn/trunk/hibernate-cache-testcase