ちょっと、Java SE環境でJTAを使ってみたくなりまして。まあ、JTAを試すのに、アプリケーションサーバーを用意したりデプロイしたりするのが面倒というだけの理由です。
JTAの実装には、Narayanaを使用することにしました。
他にもBitronixが最近は名前をよく聞くのですが、JTA 1.2への対応を見ているとNarayanaだけっぽい感じがしたので。
GitHub - bitronix/btm: JTA Transaction Manager
ちょうど、JTA × JPAのサンプルもありましたので、こちらを参考に書いてみました。
https://github.com/jbosstm/quickstart/tree/master/jta-and-hibernate
サンプルと変えているのは、サンプルは@TransactionalアノテーションとTransactionManagerでトランザクション管理をしていますが、ここではUserTransactionを使って手動でbegin/commit/rollbackすることにしてみます。
準備
ビルド定義は、以下のように。
build.sbt
name := "narayana-standalone-jpa" version := "0.0.1-SNAPSHOT" organization := "org.littlewings" scalaVersion := "2.11.8" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) libraryDependencies ++= Seq( // Nayarana JTA "org.jboss.spec.javax.transaction" % "jboss-transaction-api_1.2_spec" % "1.0.0.Final" % "runtime", "org.jboss.narayana.jta" % "narayana-jta" % "5.3.1.Final", // JNDI Server "jboss" % "jnpserver" % "4.2.2.GA", "org.jboss.logging" % "jboss-logging" % "3.3.0.Final", // JPA "org.hibernate" % "hibernate-entitymanager" % "5.1.0.Final", // H2 Database "com.h2database" % "h2" % "1.4.191", // Test "org.scalatest" %% "scalatest" % "2.2.6" % "test" )
JPAのEntity
簡単なJPAのEntityから。こちらは、以下のような実装としました。
src/test/scala/org/littlewings/javaee7/narayana/Book.scala
package org.littlewings.javaee7.narayana import javax.persistence._ import scala.beans.BeanProperty object Book { def apply(isbn: String, title: String, price: Int): Book = { val b = new Book b.isbn = isbn b.title = title b.price = price b } } @Entity @Table(name = "book") @SerialVersionUID(1L) class Book extends Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @BeanProperty var id: Long = _ @Column @BeanProperty var isbn: String = _ @Column @BeanProperty var title: String = _ @Column @BeanProperty var price: Int = _ }
テストコードの雛形
テストコードの雛形は、以下のように用意。
src/test/scala/org/littlewings/javaee7/narayana/NayaranaStandaloneJtaSpec.scala
package org.littlewings.javaee7.narayana import javax.naming.InitialContext import javax.persistence.{NoResultException, Persistence} import javax.transaction.UserTransaction import com.arjuna.ats.jta.common.jtaPropertyManager import com.arjuna.ats.jta.utils.JNDIManager import org.h2.jdbcx.JdbcDataSource import org.jnp.interfaces.NamingParser import org.jnp.server.NamingBeanImpl import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll, FunSpec, Matchers} class NayaranaStandaloneJtaSpec extends FunSpec with BeforeAndAfter with BeforeAndAfterAll with Matchers { // ここに、もろもろの初期化/終了処理やテストを書く! }
BeforeAndAfterAllトレイトをMix-inしているので、クラス単位の初期化処理および終了処理でJNDIサーバーの起動と、JTA関係のクラスやDataSourceの登録を行います。
val namingBean: NamingBeanImpl = new NamingBeanImpl override def beforeAll(): Unit = { namingBean.start() JNDIManager.bindJTATransactionManagerImplementation() namingBean.getNamingInstance.createSubcontext(new NamingParser().parse("jboss")) jtaPropertyManager.getJTAEnvironmentBean.setTransactionManagerJNDIContext("java:/jboss/TransactionManager") jtaPropertyManager .getJTAEnvironmentBean .setTransactionSynchronizationRegistryJNDIContext("java:/jboss/TransactionSynchronizationRegistry") jtaPropertyManager .getJTAEnvironmentBean .setUserTransactionJNDIContext("java:comp/UserTransaction") JNDIManager.bindJTATransactionManagerImplementation() JNDIManager.bindJTAUserTransactionImplementation() namingBean.getNamingInstance.createSubcontext(new NamingParser().parse("jdbc")) val dataSource = new JdbcDataSource dataSource.setURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1") val context = new InitialContext context.bind("java:/jdbc/h2Ds", dataSource) } override def afterAll(): Unit = { namingBean.stop() }
元のサンプルと異なるのは、UserTransactionへのコンテキストのバインドが増えていたり、jdbcサブコンテキスを作成していたりするところですね。
このコードを使ってJNDIをスタンドアロンで起動するためには、以下の設定が必要です。
src/test/resources/jndi.properties
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces
DataSourceはJNDI名を「jdbc/h2Ds」としたので、以下のようにpersistence.xmlを用意。
src/test/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <persistence 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://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" version="2.1"> <persistence-unit name="javaee7.standalone.jta.pu" transaction-type="JTA"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <jta-data-source>jdbc/h2Ds</jta-data-source> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.hbm2ddl.auto" value="update"/> </properties> </persistence-unit> </persistence>
あとは、InitialContextからUserTransactionをルックアップしてJPAを使うだけです。
コミットするケース。
it("use simple JPA, commit") { val emf = Persistence.createEntityManagerFactory("javaee7.standalone.jta.pu") val em = emf.createEntityManager val userTransaction = new InitialContext().lookup("java:comp/UserTransaction").asInstanceOf[UserTransaction] userTransaction.begin() em.persist(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104)) em.persist(Book("978-4798042169", "わかりやすいJavaEEウェブシステム入門", 3456)) em.persist(Book("978-4798124605", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION) ", 4536)) userTransaction.commit() val query = em .createQuery("SELECT b FROM Book b WHERE isbn = :isbn", classOf[Book]) .setParameter("isbn", "978-4798140926") query.getSingleResult.title should be("Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築") em.close() emf.close() }
ロールバックするケース。
it("use simple JPA, rollback") { val emf = Persistence.createEntityManagerFactory("javaee7.standalone.jta.pu") val em = emf.createEntityManager val userTransaction = new InitialContext().lookup("java:comp/UserTransaction").asInstanceOf[UserTransaction] userTransaction.begin() em.persist(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104)) em.persist(Book("978-4798042169", "わかりやすいJavaEEウェブシステム入門", 3456)) em.persist(Book("978-4798124605", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION) ", 4536)) userTransaction.rollback() val query = em .createQuery("SELECT b FROM Book b WHERE isbn = :isbn", classOf[Book]) .setParameter("isbn", "978-4798140926") val thrown = the[NoResultException] thrownBy query.getSingleResult thrown.getMessage should be("No entity found for query") em.close() emf.close() }
OKそうですね。
なお、このケースを両方同時に動かすために、beforeも仕込んでいます。
before { val emf = Persistence.createEntityManagerFactory("javaee7.standalone.jta.pu") val em = emf.createEntityManager val userTransaction = new InitialContext().lookup("java:comp/UserTransaction").asInstanceOf[UserTransaction] userTransaction.begin() em.createNativeQuery("TRUNCATE TABLE book").executeUpdate() userTransaction.commit() em.close() emf.close() }
Narayanaの設定
オマケ的に。
このまま実行すると、NarayanaがObjectStoreなどをカレントディレクトリに作成してしまうので、それもジャマだなぁと思って以下のように設定しました。
src/test/resources/jbossts-properties.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <entry key="ObjectStoreEnvironmentBean.communicationStore.objectStoreType">com.arjuna.ats.internal.arjuna.objectstore.VolatileStore</entry> <entry key="ObjectStoreEnvironmentBean.objectStoreDir">./target/ObjectStore</entry> </properties>
XML形式のプロパティファイルとか、久しぶりに書きました…。
まとめ
Java SE環境で、JNDI、JTA、JPAを使って、UserTransactionを使ったトランザクション管理を動作させてみました。実は、Narayanaの設定にちょっとてこずったり、JNDIまわりのコードを2回動作させると失敗したりと若干ハマったのですが、1ケース単体でみればけっこうすんなりいったので良かったです。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/narayana-standalone-jpa