ヒベルナ二級キャッシュは実戦的です。


この記事を通じてhibernate 2級キャッシュの使用経歴についていくつかのtest caseを利用して、2級キャッシュが使用中に注意すべき問題をコードの角度から説明します。
使用した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 Book {
	
	private Long id;
	private String title;
	
	private Author author;
        // getter setter methods omitted
}
主なテストクラスはTestHibernate SecondLevelCache.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();
	}
}
方法setUpSession Factoryを作成するために使用されます。Session Factoryを作成するのは比較的に時間がかかる操作ですので、Junnit 4の@Before Class annotationを追加して、このSession Factoryは一回だけ作成され、すべてのtest caseに共有されます。clearSedconche Cacheはキャッシュされます。前のtest caseの結果が後のtest caseに影響を与えることを防止します。
テストで使用した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();
	}
testSession GetCacheはまずcreateAuthorを通じてauthorオブジェクトを作成して、astertGetMissCacheの中でauthor.idを使ってgetメソッドを使って検出する前に作成したauthorです。これは毎回getメソッドを呼び出すので、hibernateはデータベースからauthorオブジェクトを取り戻します。二級キャッシュに入れます。テスト結果はhibernate statistics統計情報の中のsecond level cache miss countによって判断されます。今回のgetクエリはキャッシュに命中していません。
次にastertGetHitCacheは同じidでauthorオブジェクトをget方法で取得します。このidのオブジェクトは以前に2段階のキャッシュに保存されていますので、今回の操作はキャッシュに命中します。
最後にudateAuthorで更新される前のauthorオブジェクトによって、hibernateは自動的にオブジェクトを二段階キャッシュからクリアしますので、getメソッドを3回目に呼び出した時にキャッシュに命中しませんでした。
まとめ: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());
	}

同様の結果、ID_loadを通じてキャッシュに命中しなかった場合、2回目は同じIDでロード方法を呼び出してキャッシュに命中し、authorオブジェクトを更新した後にキャッシュが失効し、3回目のクエリーはデータベースからauthorを取得する。
一つの点はget方法と違います。
Author author = (Author) session.load(clazz, id);
		author.getName();
まとめ:ロードメソッドを呼び出した時、hibernateは最初はキャッシュやデータベースを調べていませんでしたが、先にプロキシのオブジェクトに戻ります。このオブジェクトはidのみを含み、呼び出し対象の非id属性が表示された場合、author.getName()、hibernateはキャッシュに行って検索します。キャッシュに命中していない場合はデータベースを探してください。データベースが見つからない場合は異常です。ロード方法はできるだけ対象の検索作業を遅らせることができます。これはget方法との最大の違いです。
この2つのテストケースは以下の通りです。
@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.reat Query().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());
	}
まず2つのauthorオブジェクトを作成し、HQLを使用します。「from Author」でlistメソッドを呼び出します。このときhibernateは直接データベースからすべてのauthorオブジェクトを調べます。キャッシュからは調べられませんでしたが、listメソッドで検出したすべてのauthorオブジェクトは2級キャッシュに保存されます。これはgetsStatistics()を通じてgets SecondLevelCacheputCount SelCachect(見ることができます。)
次にリストメソッドをもう一度呼び出します。この時はまだクエリキャッシュを開けていませんので、リスト方法はもう一度データから調べました。最初のクエリはすべてのauthorをキャッシュに入れましたので、再度getメソッドを呼び出すとキャッシュに命中します。astertGetHitCacheは通過します。
まとめ:listメソッドは二級キャッシュからは検索されませんが、データベースから検索されたオブジェクトはcacheに保存されます。
四.session.reat Query().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());
	}
まず2つのauthorオブジェクトを作成し、HQL:"from Author"でiterateメソッドを呼び出します。この時hibernateはauthorオブジェクトを調べていません。まずデータベースからauthorのidをすべて検出して、コンソールは以下のSQLを入力します。
select id from author
iteratorの中を巡回する時、idによって一つ一つキャッシュからauthorを探します。見つけられませんでした。またデータベースにアクセスします。
まとめ:iterate方法は典型的なN+1回のクエリを使用しています。まず、データベースからすべてのオブジェクトのIDを検索し、IDに従って1つずつ2級のキャッシュから検索します。2級のキャッシュは見つからないです。
五.assicache
hibernateサポートは関連をキャッシュし、まずBook.javaにブックセットのキャッシュプロファイルを追加します。
@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();
	}
まずauthorを作成し、authorのために3つのbookオブジェクトを追加します。astertGetBooks ForAuthormissCacheでauthor.getBook sを通じて関連のbookセットにアクセスします。遅延負荷の関係で、ここではクエリキャッシュも検索データベースもないので、a.getBook()を呼び出します。size(つまり、最初にbookのセットにアクセスします。データベースクエリを再構築し、生成されたSQLは以下のようになります。
select * from book where author_id = ?
この時statisticsのmissCountは1だけ増加しました。author.get Book sを呼び出してもキャッシュに命中しませんでした。hibernateはデータベースからbook sを検索した後、book s関連及び3つのbookオブジェクトを2級キャッシュに保存します。
関連のキャッシュはどのような形で存在しますか?注意関連キャッシュはブックセット自体を保存せずに、すべてのbookのIDを保存しています。3つのbookオブジェクトのIDがそれぞれ1,2,3であると仮定すると、authorキャッシュのフォーマットは以下のようになります。
*---------------------------------*
|        Author Data Cache        |
|---------------------------------|
| 1 -> [ "septem" , [ 1, 2, 3 ] ] |
*---------------------------------*
第二ステップでastertGetBook s ForAuthorHitCacheを実行した時、hit Countが4つ増えました。第二回author.get Book sを呼び出した時、関連キャッシュに命中しました。キャッシュから3つのidを取りました。また、それぞれidで1つずつ二段キャッシュから3つのbookオブジェクトを取り出して、全部で4回キャッシュしました。
続いてudateOnebook ForAuthorによってその中の一つのbookオブジェクトを更新しました。仮に更新したのはidが1のbookです。次のastert GetBook sForAuthormissCache(author,1)方法の中でmissCountはまた1.bookを更新しましたが、author.geot.Bodkはリストに登録されますか?book IDでbookを検索したところ、IDが1のbookに更新されましたので、その2級キャッシュは失効しました。再びデータベースに取りに行きます。この時、missCountは1増えました。idは2,3のbookですか?それとも2級キャッシュから見つけました。この方法はhibernateに以下のようなSQLが生成されます。
select * from book where id = 1
その中の一つのbookオブジェクトを更新すると、関連キャッシュは失効しません。ただし、集合IDリストを更新すると、キャッシュは失効します。まずaddNewBook ForAuthorを通じてauthorのためにbookオブジェクトを追加します。この時、bookグループの中には全部で4つのbookオブジェクトがあります。最後のastertGetkbook sForAuthormiche(author)missCountは1を追加しました。この時初めてauthor.get Book sを呼び出したのと同じように、hibernateは次のようなSQLを生成します。
select * from book where author_id = ?
まとめ:関連キャッシュは、セット自体ではなく、セットのidリストを保存しています。関連付けられたキャッシュは、IDによって1つずつ2つのキャッシュから検索しています。検索データベースが見つかりません。更新セットのいずれかのオブジェクトは、関連キャッシュが無効になりません。セットを変更したidリストだけがあれば、キャッシュが無効になります。
五.クエリキャッシュquery cache
hibernate.cfg.xmlに以下の設定を加えてクエリーキャッシュをオープンします。
<property name="hibernate.cache.use_query_cache">true</property>
テストの用例は以下の通りです
@Test
	public void testQueryCache(){
		createAuthor();
		createAuthor();
		
		assertQueryMissCache();
		
		assertQueryHitCache();
		
		createAuthor();
		assertQueryMissCache();
	}
まず準備作業をして、2つのauthorオブジェクトを作成します。IDがそれぞれ1,2.2 astertQuery MissCacheの中で初めてlistメソッドを呼び出します。リストを呼び出す前に、set Cachebale(true)でなければ、クエリーキャッシュは使えません。この時、hibernateはデータベースからAuthorオブジェクトを調べて、今回のクエリをキャッシュに保存します。同時に照会したauthorオブジェクトを二級キャッシュに保存します。
クエリーキャッシュは、クエリー結果セットを保存せず、結果セットのidだけを保存します。その構造は以下のデータと同じです。
*---------------------------------------------------------------*
|                         Query Cache                           |
|---------------------------------------------------------------|
| [ ["from Author where name = ?", [ "septem"] ] -> [  1, 2 ] ] |
*---------------------------------------------------------------*
注意キャッシュのkeyは、HQL、パラメータおよび改ページパラメータに関連しています。
また、astertQueryHitCache()を呼び出して、同じHQLとパラメータでAuthorを再検索します。この時、クエリキャッシュに命中します。そして、結果集idによってauthorオブジェクトを一つずつ調べます。authorオブジェクトは以前にキャッシュに入れられていましたので、今回のクエリもセカンダリキャッシュに命中します。
キャッシュの失効は特殊です。クエリーに関するテーブルのデータが変化すると、キャッシュは失効します。例えば、Authorオブジェクトをもう一つ作成します。Authorテーブルは変化します。元のキャッシュは失効します。
まとめ:クエリーキャッシュのkeyはHQL、クエリーパラメータおよび分布パラメータに関連しています。また、クエリーに関連するテーブルのデータが変化するとキャッシュは失効します。したがって、生産環境において命中率が低いです。クエリーキャッシュは結果セットのidリストであり、結果セット自体ではなく、キャッシュに命中すると、idによって一つずつ第二級キャッシュから検索します。見つからないので、データベースを調べます。
関連コードはすべてgoogleコードに保存されています。
svn checkout http://hibernate-cache-testcase.googlecode.com/svn/trunk/ hibernate-cache-testcase