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