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&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 EEでJTAを含めて環境的に統合してしまうと、もしかして動きが違うのかもしれませんが。
あとは、クラスタにしたりすると、よく楽観的ロックにひっかかったりします…。これは、自分の書き方が悪いのかなぁ…?
ちょっと、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" }