ヒベルナ二級キャッシュ(三)


この記事では、hibernate 2級キャッシュのいくつかの使用履歴を利用して、いくつかのtest caseを利用して、コードの観点から2級キャッシュを説明します。使用中に注意すべき問題があります。
@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()を見てみます。
@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クエリは、キャッシュに当たらないと判断されます。次に、astertGetHit Cacheは同じidでauthorオブジェクトをget方法で取得します。このidの対象の前にはすでに2級キャッシュに保存されています。したがって、今回の操作の命中キャッシュは最後にudateAuthorによって更新される前のauthorオブジェクトによって、hibernateは自動的にこのオブジェクトを二段キャッシュからクリアします。したがって、3回目の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());
    }
 
同様の結果、ID_loadを通じてキャッシュに命中しなかった場合、2回目は同じIDでロード方法を呼び出してキャッシュに命中し、authorオブジェクトを更新した後にキャッシュが失効し、3回目のクエリーはデータベースを通じてauthorを取得する点はget方法と異なる。
Author author = (Author) session.load(clazz, id);
        author.getName();
 まとめ:ロードメソッドを呼び出した時、hibernateは最初はキャッシュやデータベースを調べていませんでしたが、先にプロキシのオブジェクトに戻ります。このオブジェクトはidのみを含み、呼び出し対象の非id属性が表示された場合、author.getName()、hibernateはキャッシュに行って検索します。キャッシュに命中していない場合はデータベースを探してください。データベースが見つからない場合は異常です。ロード方法はできるだけ対象の検索作業を遅らせることができます。これは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);
    }
 
三.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つのキャッシュに保存されます。この時はまだクエリキャッシュを開けていませんので、リストメソッドはもう一度データから調べました。最初のクエリはすべてのauthorをキャッシュに入れました。ですから、getメソッドを呼び出したらキャッシュに命中します。astertGetHitCacheはまとめによって、listメソッドは2級キャッシュから検索されませんが、データベースから検索されたオブジェクトはcache4.session.createQuete.setury.
@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によって一つずつ二級キャッシュから検索します。二級キャッシュが見つからないなら、再照会データベース5.assicache cache hibernateサポートは関連をキャッシュし、まずBook.javaにbook.sセットのキャッシュプロファイルを追加します。
@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を呼び出してもキャッシュに命中しませんでした。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を実行した時、私達はhitosh Countが4増加したのを見ました。第二回author.get Book sを呼び出した時、関連キャッシュに命中しました。キャッシュから3つのidを取り戻しました。またそれぞれidで1つずつ2級キャッシュから3つのbookオブジェクトを取り出して、その後Forthotebookをキャッシュします。その中の一つのbookオブジェクトを更新しました。仮に更新したのはidが1のbookです。次のastertGetBook s ForAuthormissCache(author,1)方法でmissCountはまた1.bookが更新されましたが、author.getBook sはキャッシュに命中しました。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オブジェクトは以前にキャッシュに預けられていましたので、今回のクエリも2つのキャッシュに命中します。キャッシュは失効します。例えば、Authorオブジェクトをもう一つ作成します。この時Authorテーブルが変化しました。元のキャッシュは失効しました。まとめ:クエリキャッシュのkeyはHQL、クエリパラメータ及び分布パラメータに関連しています。また、照会に関わるテーブルのデータが変化したら、キャッシュは失効します。ですから、生産環境では命中率が低いです。検索キャッシュには結果集のidリストが保存されています。結果集自体ではなく、キャッシュに命中した時は、idによって一つ一つ第二級キャッシュから検索します。検索データベースが見つかりませんでした。google code上のsvn chocoutに関連するすべてのコードが保存されています。http://hibernate-cache-testcase.googlecode.com/svn/trunk/ ヒベルナ-cache-testcase