CLOVER🍀

That was when it all began.

InfinispanのJPA Cache Storeを試す

InfinispanにいくつかあるCache Storeの中でも、ちょっと気になっていたものです。

JPA Cache Store
http://infinispan.org/docs/cachestores/jpa/

Infinispanは、JPAをCache Storeとすることができます(JPAバージョンは2.0)。なんのことか、コードのイメージを見るまではわかりませんでしたが…。

通常、InfinispanとJPAを組み合わせる時は、JPAの2nd Level Cacheとして使用するかと思います。ただ、この方法とは別にEntityをCacheに格納することで、そのままCacheの保存先としてテーブルにマップできるという話ですね。

JDBC Cache Storeとの違いは、バイナリでデータを保存するのに対して、JPA Cache Storeは背後にEntityManagerがいて、Entityに対応するテーブルとやり取りします。

JPA Cache Storeの注意事項として、

  • EntityにはID定義が必要なこと
  • Entityには、ひとつの@Idまたは@EmbeddedIdアノテーションのみが許可される
  • IDの自動生成はサポートしていない
  • Cacheに格納したEntityは、すべて永続化される

IDの自動生成をサポートしていないのは、Cacheに登録する時にキーが必要だからですよね…。

では、ちょっと試してみましょう。

build.sbt

name := "infinispan-cachestore-jpa-example"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.3"

organization := "littlewings"

fork in run := true

connectInput := true

resolvers += "Public JBoss Group" at "http://repository.jboss.org/nexus/content/groups/public-jboss"

libraryDependencies ++= Seq(
  "org.hibernate" % "hibernate-entitymanager" % "4.2.7.SP1",
  "org.infinispan" % "infinispan-cachestore-jpa" % "5.3.0.Final",
  "net.jcip" % "jcip-annotations" % "1.0",
  "mysql" % "mysql-connector-java" % "5.1.26" % "runtime"
)

データベースは、MySQLです。

JPAの設定。
src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
  <persistence-unit name="infinispan.cachestore.jpa.example" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <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="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/test?useUnicode=true&amp;characterEncoding=utf8" />
      <property name="javax.persistence.jdbc.user" value="kazuhira" />
      <property name="javax.persistence.jdbc.password" value="password" />
    </properties>
  </persistence-unit>
</persistence>

Hibernateが発行したSQLを見るために、「hibernate.show_sql」と「hibernate.format_sql」を有効にしました。まあ、「format_sql」は単に整形してくれるだけですが。

Infinispanの設定。
src/main/resources/infinispan.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="urn:infinispan:config:5.3 http://www.infinispan.org/schemas/infinispan-config-5.3.xsd"
    xmlns="urn:infinispan:config:5.3">

  <global>
    <transport clusterName="jpa-cachestore-cluster">
      <properties>
        <property name="configurationFile" value="jgroups.xml" />
      </properties>
    </transport>
    <globalJmxStatistics
        enabled="true"
        jmxDomain="org.infinispan"
        cacheManagerName="DefaultCacheManager"
        />
  </global>

  <namedCache name="cacheStoreJpa">
    <jmxStatistics enabled="true" />
    <clustering mode="distribution" />

    <loaders passivation="false" shared="true" preload="true">
      <jpaStore xmlns="urn:infinispan:config:jpa:5.3"
                persistenceUnitName="infinispan.cachestore.jpa.example"
                entityClassName="User" />
    </loaders>
  </namedCache>
</infinispan>

トランザクショナルなCacheにしようかとも思ったのですが、ちょっと理由があって対象外に。

jpaStoreタグには、XML名前空間の指定が必要なことに注意です。「persistenceUnitName」属性には使用する永続性コンテキスト名を設定し、「entityClassName」属性にはCacheに格納するEntityクラスを設定します。

ちなみに、loadersタグの「preload」はtrueにするとCacheの起動時に、この場合JPA Cache Storeを使っているのでデータベースからデータをロードにCacheに載せるようになります。また、Infinispanを複数Node起動した場合には背後のデータベースおよびテーブルは共有することになるので、「shared」属性をtrueにします。

passivationはfalseでよいですね。

JGroupsの設定は端折ります。

では、JPA Cache Storeを使ったコード。Entityはこんなのです。

object User {
  def apply(id: Int, firstName: String, lastName: String, age: Int): User = {
    val user = new User
    user.id = id
    user.firstName = firstName
    user.lastName = lastName
    user.age = age
    user
  }
}

@SerialVersionUID(1L)
@Entity
@Table(name = "user")
class User extends Serializable {
  @Id
  @BeanProperty
  var id: Int = _

  @Column(name = "first_name")
  @BeanProperty
  var firstName: String = _

  @Column(name = "last_name")
  @BeanProperty var lastName: String = _

  @Column
  @BeanProperty
  var age: Int = _

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

  override def toString(): String =
    s"id = $id, firstName = $firstName, lastName = $lastName, age = $age, versionNo = $versionNo"
}

DefaultCacheManagerは普通に生成しますが、Cacheの型パラメータは主キーの型とEntityになります。

    val manager = new DefaultCacheManager("infinispan.xml")

    try {
      val cache = manager.getCache[Int, User]("cacheStoreJpa")

putをするとinsertやupdateになり

      val user = User(1, "カツオ", "磯野", 11)
      cache.put(user.id, user)

clearすると、テーブルからもデータを削除します。

      // Cache#clearすると、全データをDELETEします
      cache.clear()

Cache#removeはうまく機能しなかったような…。Cacheからは消えましたが、テーブルからは削除されなかったです。そういうものっぽい?

あと、別途JPAを使うとCache Storeに保存したEntityを直接取得したり、更新したりすることはできますが

      val entityManagerFactory = Persistence.createEntityManagerFactory("infinispan.cachestore.jpa.example")
      val entityManager = entityManagerFactory.createEntityManager()
      val emTx = entityManager.getTransaction

      emTx.begin()

      val query = entityManager.createQuery("SELECT u FROM User u")

      query.getResultList.asScala.foreach {
        case u: User =>
          println(s"Selected JPA: $u")
          u.age += 1
          // ここで更新
          entityManager.merge(u)
          println(s"Updated JPA: $u")

          // 年齢は増えている
          require(u.age == 12)
      }

      emTx.commit()

      entityManager.close()
      entityManagerFactory.close()

その後、JPA Cache Storeを使うと楽観的ロックに引っかかってエラーとなります。

      // JPAを直接使って更新してしまうと、実はここでロードしようとしてもCacheからはズレてしまう…
      println("===== Current Cache2 =====")
      println(cache.get(user.id))
      require(cache.get(user.id).age == 11)  // 年齢が増えていない
      println("===== Current Cache2 =====")

      try {
        val u = cache.get(user.id)
        u.age += 1
        cache.put(u.id, u)

        require(cache.get(u.id).age == 12)
      } catch {
        // 上記JPAのコードを実行した場合は、
        // 管理されている状態とズレているので、例外となる
        case e: CacheException => println(s"Exception => ${e.getMessage}")
      }

実際、JPAで直接更新した後に、Cache#getしても変更は反映されていませんでしたし。今回はJava SE環境でやっているので、Java EEJTAを含めて環境的に統合してしまうと、もしかして動きが違うのかもしれませんが。

あとは、クラスタにしたりすると、よく楽観的ロックにひっかかったりします…。これは、自分の書き方が悪いのかなぁ…?

ちょっと、Java SEで試すべきではなかったかもしれません。

とはいえ、JPAのフロントエンドとしてInfinispanが使えるので、けっこう面白い試みだとは思うのですが。
src/main/scala/InfinispanCacheStoreJpa.scala

import scala.collection.JavaConverters._

import scala.beans.BeanProperty

import javax.persistence.{Column, Entity, Id, Persistence, Version}

import org.infinispan.CacheException
import org.infinispan.manager.DefaultCacheManager

object InfinispanCacheStoreJpaExample {
  def main(args: Array[String]): Unit = {
    val manager = new DefaultCacheManager("infinispan.xml")

    try {
      println("===== Create Cache =====")
      val cache = manager.getCache[Int, User]("cacheStoreJpa")
      println("===== Created Cache =====")

      // Cache#clearすると、全データをDELETEします
      cache.clear()

      val user = User(1, "カツオ", "磯野", 11)
      cache.put(user.id, user)

      println("===== Current Cache1 =====")
      println(cache.get(user.id))
      require(cache.get(user.id).age == 11)
      println("===== Current Cache1 =====")

      /*
      // JPAで直接更新
      println("===== JPA Update =====")
      val entityManagerFactory = Persistence.createEntityManagerFactory("infinispan.cachestore.jpa.example")
      val entityManager = entityManagerFactory.createEntityManager()
      val emTx = entityManager.getTransaction

      emTx.begin()

      val query = entityManager.createQuery("SELECT u FROM User u")

      query.getResultList.asScala.foreach {
        case u: User =>
          println(s"Selected JPA: $u")
          u.age += 1
          // ここで更新
          entityManager.merge(u)
          println(s"Updated JPA: $u")

          // 年齢は増えている
          require(u.age == 12)
      }

      emTx.commit()

      entityManager.close()
      entityManagerFactory.close()
      println("===== JPA Updated =====")
      */

      // JPAを直接使って更新してしまうと、実はここでロードしようとしてもCacheからはズレてしまう…
      println("===== Current Cache2 =====")
      println(cache.get(user.id))
      require(cache.get(user.id).age == 11)  // 年齢が増えていない
      println("===== Current Cache2 =====")

      try {
        val u = cache.get(user.id)
        u.age += 1
        cache.put(u.id, u)

        require(cache.get(u.id).age == 12)
      } catch {
        // 上記JPAのコードを実行した場合は、
        // 管理されている状態とズレているので、例外となる
        case e: CacheException => println(s"Exception => ${e.getMessage}")
      }

      cache.stop()
    } finally {
      manager.stop()
    }
  }
}

object User {
  def apply(id: Int, firstName: String, lastName: String, age: Int): User = {
    val user = new User
    user.id = id
    user.firstName = firstName
    user.lastName = lastName
    user.age = age
    user
  }
}

@SerialVersionUID(1L)
@Entity
@Table(name = "user")
class User extends Serializable {
  @Id
  @BeanProperty
  var id: Int = _

  @Column(name = "first_name")
  @BeanProperty
  var firstName: String = _

  @Column(name = "last_name")
  @BeanProperty var lastName: String = _

  @Column
  @BeanProperty
  var age: Int = _

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

  override def toString(): String =
    s"id = $id, firstName = $firstName, lastName = $lastName, age = $age, versionNo = $versionNo"
}