CLOVER🍀

That was when it all began.

Java SE環境で、JNDI × JTA(Narayana) × JPA

ちょっと、Java SE環境でJTAを使ってみたくなりまして。まあ、JTAを試すのに、アプリケーションサーバーを用意したりデプロイしたりするのが面倒というだけの理由です。

JTAの実装には、Narayanaを使用することにしました。

Narayana Homepage · 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の実装はHibernate、データベースはH2を利用します。また、JNDIサーバーも必要になります。

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形式のプロパティファイルとか、久しぶりに書きました…。

参考)
3.5. Configuration options

まとめ

Java SE環境で、JNDI、JTAJPAを使って、UserTransactionを使ったトランザクション管理を動作させてみました。実は、Narayanaの設定にちょっとてこずったり、JNDIまわりのコードを2回動作させると失敗したりと若干ハマったのですが、1ケース単体でみればけっこうすんなりいったので良かったです。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/narayana-standalone-jpa