CLOVER🍀

That was when it all began.

JPAのSecond Level Cacheを試してみる

WildFlyもリリースされ、Infinispan 6.0.1.Finalが同梱されていることですので、JPA(実装はHibernate)のSecond Level Cache(以降、L2キャッシュ)を使ってみることにしました。

以前にJBoss AS 7.1.1でやっていたころは、うまくいかなかったんですよねぇ…。

そもそもL2キャッシュは?というと、

  • エンティティのデータをキャッシュするもの
  • クエリとその結果をキャッシュする、クエリキャッシュ

の2つがあり、EntityManagerが管理するL1キャッシュを越えた範囲のキャッシュ機能を提供する仕組みみたいです。

準備

まずは、依存関係の定義。
build.sbt

name := "jpa-l2-cache"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.3"

organization := "org.lilttlewings"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked")

seq(webSettings: _*)

artifactName := { (version: ScalaVersion, module: ModuleID, artifact: Artifact) =>
  //artifact.name + "." + artifact.extension
  "javaee7-web." + artifact.extension
}

{
val jettyVersion = "9.1.2.v20140210"
libraryDependencies ++= Seq(
  "org.eclipse.jetty" % "jetty-webapp" % jettyVersion % "container",
  "org.eclipse.jetty" % "jetty-plus"   % jettyVersion% "container",
  "javax" % "javaee-web-api" % "7.0" % "provided"
)
}

xsbt-web-pluginも使用します。
project/plugins.sbt

addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "0.7.0")

JPAの設定

L2キャッシュを有効にするために、JPAの設定を以下の様にします。
src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                                 http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/persistence/persistence_2_1.xsd">
  <persistence-unit name="javaee7.web.pu" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>java:jboss/datasources/mysqlXaDs</jta-data-source>
    <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
    <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
      <property name="hibernate.show_sql" value="true" />
      <property name="hibernate.format_sql" value="true" />
      <property name="hibernate.cache.use_second_level_cache" value="true" />
      <property name="hibernate.cache.use_query_cache" value="true" />
    </properties>
  </persistence-unit>
</persistence>

ポイントは、ここと

    <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>

ここですね。

      <property name="hibernate.cache.use_second_level_cache" value="true" />
      <property name="hibernate.cache.use_query_cache" value="true" />

shared-cache-modeは、

  • ALL … すべてのエンティティおよびエンティティ関連のデータをキャッシュ
  • DISABLE_SELECTIVE … @Cacheable(false)を付けたエンティティを除く、すべてのエンティティをキャッシュ
  • ENABLE_SELECTIVE … @Cacheable(true)を付けた、すべてのエンティティをキャッシュ
  • NONE … キャッシュは無効

で設定できるそうな。推奨は、ENABLE_SELECTIVEみたい?

参考)
Cache mappings
http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html_single/#performance-cache-mapping

プロパティ「hibernate.cache.use_second_level_cache」をtrueにすることで、L2キャッシュを有効にします。

また、プロパティ「hibernate.cache.use_query_cache」をtrueにすることで、クエリキャッシュを有効にできるようなのですが、どうもこれだけだと足りないみたいです。

参考)
3.3. JDBC connectionsのTable 3.5. Hibernate Cache Properties
http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html_single/#configuration-hibernatejdbc

EntityおよびService

キャッシュするEntity。
src/main/scala/org/littlewings/javaee7/entity/Book.scala

package org.littlewings.javaee7.entity

import scala.beans.BeanProperty

import javax.persistence.Cacheable
import javax.persistence.{Column, Entity, Id, Table, Version}

@SerialVersionUID(1L)
@Entity
@Table(name = "book")
@Cacheable(true)
class Book extends Serializable {
  @Id
  @Column
  @BeanProperty
  var isbn: String = _

  @Column
  @BeanProperty
  var title: String = _

  @Column
  @BeanProperty
  var price: Int = _

  @Column
  @BeanProperty
  var summary: String = _

  @Column(name = "version_no")
  @BeanProperty
  @Version
  var versionNo: Int = _

  override def toString: String =
    s"isbn = $isbn, title = $title, price = $price, summary = $summary, versionNo = $versionNo"
}

以下のアノテーションを付与しているところが、ポイントです。

@Cacheable(true)

なお、@Cacheableアノテーションのデフォルト値はtrueなので、明示しなくても意味は同じです。

続いて、EntityManagerを操作するServiceクラス。
src/main/scala/org/littlewings/javaee7/service/BookService.scala

package org.littlewings.javaee7.service

import scala.collection.JavaConverters._

import javax.enterprise.context.RequestScoped
import javax.persistence.{EntityManager, PersistenceContext}
import javax.transaction.Transactional

import org.littlewings.javaee7.entity.Book

@Transactional
@RequestScoped
class BookService {
  @PersistenceContext
  private var em: EntityManager = _

  def create(book: Book): Unit =
    em.persist(book)

  def update(book: Book): Book =
    em.merge(book)

  def remove(book: Book): Unit =
    em.remove(em.merge(book))

  def findByIsbn(isbn: String): Book =
    em.find(classOf[Book], isbn)

  def findAll: Iterable[Book] =
    em
      .createQuery(s"""|SELECT b
                       |  FROM Book b""".stripMargin)
      .setHint("org.hibernate.cacheable", true)
      .setHint("org.hibernate.cacheMode", "NORMAL")
      .getResultList
      .asScala
      .asInstanceOf[Iterable[Book]]

  def findByOverPrice(price: Int): Iterable[Book] =
    em
      .createQuery(s"""|SELECT b
                       |  FROM Book b
                       | WHERE b.price >= :price
                       | ORDER BY b.price ASC""".stripMargin)
      .setParameter("price", price)
      .setHint("org.hibernate.cacheable", true)
      .setHint("org.hibernate.cacheMode", "NORMAL")
      .getResultList
      .asScala
      .asInstanceOf[Iterable[Book]]
}

今回からJava EE 7を使用していますので、EJBではなく普通のJavaクラスにして、JTAのアノテーションを付与しています。

@Transactional
@RequestScoped

@RequestScopedアノテーションはCDIのものですが、Java EE 7からデフォルトは明示しなくちゃいけないんでしたね。beans.xmlは今回、書いてませんし。

また、クエリキャッシュですが、Hibernateの場合、Queryに以下の様に設定しないとクエリキャッシュが働かなかったように思います。

  def findAll: Iterable[Book] =
    em
      .createQuery(s"""|SELECT b
                       |  FROM Book b""".stripMargin)
      .setHint("org.hibernate.cacheable", true)
      .setHint("org.hibernate.cacheMode", "NORMAL")
      .getResultList
      .asScala
      .asInstanceOf[Iterable[Book]]

CacheModeの意味は、こちらを参照。
http://docs.jboss.org/hibernate/orm/4.3/javadocs/org/hibernate/CacheMode.html

また、Hibernateのクエリキャッシュについての参考情報。

http://stackoverflow.com/questions/21600340/how-to-enable-query-cache-in-jboss-7-1-1
https://community.jboss.org/thread/236841

http://docs.oracle.com/cd/E21043_01/core.1111/b61006/toplink.htm#BCGEGHGE
http://en.wikibooks.org/wiki/Java_Persistence/Caching#JPA_2.0_Cache_APIs

最初、Query.setHintでjavax.persistence.CacheRetrieveModeとかを使ってみて、全然効かなくてハマりました。

JAX-RS

あとは、リクエストを受け付けるJAX-RSのコードを。
src/main/scala/org/littlewings/javaee7/rest/BookResource.scala

package org.littlewings.javaee7.rest

import scala.collection.JavaConverters._

import javax.inject.Inject
import javax.ws.rs.{DELETE, GET, Path, PathParam, POST, PUT, Produces}
import javax.ws.rs.core.MediaType

import org.littlewings.javaee7.entity.Book
import org.littlewings.javaee7.service.BookService

@Path("book")
class BookResource {
  @Inject
  private var bookService: BookService = _

  @PUT
  @Path("create")
  @Produces(Array(MediaType.APPLICATION_JSON))
  def createAll(books: java.util.List[Book]): java.lang.Iterable[Book] = {
    books.asScala.foreach(bookService.create)
    books
  }

  @POST
  @Path("inc-price/{isbn}")
  @Produces(Array(MediaType.APPLICATION_JSON))
  def update(@PathParam("isbn") isbn: String): Book = {
    val book = bookService.findByIsbn(isbn)
    book.price += 1000
    bookService.update(book)
  }

  @GET
  @Path("{isbn}")
  @Produces(Array(MediaType.APPLICATION_JSON))
  def find(@PathParam("isbn") isbn: String): Book =
    bookService.findByIsbn(isbn)

  @GET
  @Path("price/{price}")
  @Produces(Array(MediaType.APPLICATION_JSON))
  def findByAge(@PathParam("price") price: Int): java.lang.Iterable[Book] =
    bookService.findByOverPrice(price).asJava

  @GET
  @Produces(Array(MediaType.APPLICATION_JSON))
  def findAll: java.lang.Iterable[Book] =
    bookService.findAll.asJava

  @DELETE
  @Path("remove/{isbn}")
  @Produces(Array(MediaType.TEXT_PLAIN))
  def remove(@PathParam("isbn") isbn: String): String = {
    bookService.remove(bookService.findByIsbn(isbn))
    "OK" + System.lineSeparator
  }

  @DELETE
  @Path("remove-all")
  @Produces(Array(MediaType.TEXT_PLAIN))
  def removeAll(): String = {
    bookService.findAll.foreach(bookService.remove)
    "OK" + System.lineSeparator
  }
}

JAX-RSの有効化。
src/main/scala/org/littlewings/javaee7/rest/JaxrsApplication.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.ApplicationPath
import javax.ws.rs.core.Application

@ApplicationPath("/rest")
class JaxrsApplication extends Application

動作確認

あとは、パッケージングしてWildFlyにデプロイします。

> package
[info] Compiling 1 Scala source to /xxxxx/target/scala-2.10/classes...
[info] Packaging /xxxxx/target/scala-2.10/javaee7-web.war ...
[info] Done packaging.

データは、以下のようなものをPUTしました。
books.json

[
    {
        "isbn": "978-4798124605",
        "title": "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava",
        "price": 4410,
        "summary": "エンタープライズJava入門書の決定版!Java EE 6は、大規模な情報システム構築に用いられるエンタープライズ環境向けのプログラミング言語です。"
    },
    {
        "isbn": "978-4798120546",
        "title": "マスタリングJavaEE5 第2版",
        "price": 5670,
        "summary": "EJB3.0、JPA、JSF、Webサービスを完全網羅。新たにJBoss AS、Hibernateにも対応!JavaEE5は、J2EEの高い機能性はそのままに、アプリケーションの開発生産性を高めることを主眼とした、サーバサイドJavaにおけるプラットフォーム、開発、デプロイメントに関する標準仕様です。"
    },
    {
        "isbn": "978-4873114675",
        "title": "JavaによるRESTfulシステム構築",
        "price": 3360,
        "summary": "Java EE 6でサポートされたJAX-RSの特徴とRESTfulアーキテクチャ原則を使って、Javaでの分散Webサービスを設計開発する方法を学ぶ書籍。"
    },
    {
        "isbn": "978-4774127804",
        "title": "Apache Lucene 入門 Java・オープンソース・全文検索システムの構築",
        "price": 3360,
        "summary": "Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。"
    },
    {
        "isbn": "978-4774161631",
        "title": "[改訂新版] Apache Solr入門 オープンソース全文検索エンジン",
        "price": 3780,
        "summary": "最新版Apaceh Solr Ver.4.5.1に対応するため大幅な書き直しと原稿の追加を行い、現在の開発環境に合わせて完全にアップデートしました。Apache Solrは多様なプログラミング言語に対応した全文検索エンジンです。"
    },
    {
        "isbn": "978-1933988177",
        "title": "Lucene in Action",
        "price": 5548,
        "summary": "When Lucene first hit the scene five years ago, it was nothing short of amazing. By using this open-source, highly scalable, super-fast search engine, developers could integrate search into applications quickly and efficiently."
    }
]

データ登録。コンテキストパスは、「javaee7-web」です。

$ curl -X PUT -H "Content-Type: application/json;" -d@books.json http://localhost:8080/javaee7-web/rest/book/create

この後、例えば全件取得すると

$ curl http://localhost:8080/javaee7-web/rest/book/

後ろのWildFlyで以下のようなログが出ますが、

17:23:28,245 INFO  [stdout] (default task-11) Hibernate: 
17:23:28,245 INFO  [stdout] (default task-11)     select
17:23:28,246 INFO  [stdout] (default task-11)         book0_.isbn as isbn1_0_,
17:23:28,246 INFO  [stdout] (default task-11)         book0_.price as price2_0_,
17:23:28,246 INFO  [stdout] (default task-11)         book0_.summary as summary3_0_,
17:23:28,246 INFO  [stdout] (default task-11)         book0_.title as title4_0_,
17:23:28,246 INFO  [stdout] (default task-11)         book0_.version_no as version_5_0_ 
17:23:28,246 INFO  [stdout] (default task-11)     from
17:23:28,247 INFO  [stdout] (default task-11)         book book0_

2回目以降は、キャッシュされているので出なくなります。もちろんQuery.setHintをやめると、常にデータベースアクセスしにいくようになりますが。

また、Queryを使わないEntityManager#findへのアクセスも、キャッシュに乗っていればやはりSQLログは出なくなります。

$ curl http://localhost:8080/javaee7-web/rest/book/978-4873114675

他にもいろいろハマりどころがありそうで、ちょっと使いこなしが難しそうな印象を受けましたが…とりあえず、動きましたよっと。

今回作成したソースコード+αは、以下にアップしています。

https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/jpa-l2-cache