Apache DeltaSpikeには、Data Moduleというものがあります。
サンプルを見ていると雰囲気はなんとなくわかるのですが、JPAでRepositoryパターンを実装するためのもので、
といった、どこかで見たような機能が使えるようになります(Spring Data JPA)。
で、せっかくなので今回のエントリはこういう構成でいってみたいと思います。
以降、順に書いていきます。
Data Moduleを使ってみる
それでは、まずはData Moduleを使ってみましょう。
準備
Maven依存関係としては、以下のように定義します。
<!-- Scala --> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>${scala.version}</version> </dependency> <!-- Java EE Web Profile --> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> <!-- Apache DeltaSpike Core & Data Module --> <dependency> <groupId>org.apache.deltaspike.core</groupId> <artifactId>deltaspike-core-api</artifactId> <version>1.7.1</version> </dependency> <dependency> <groupId>org.apache.deltaspike.core</groupId> <artifactId>deltaspike-core-impl</artifactId> <version>1.7.1</version> </dependency> <dependency> <groupId>org.apache.deltaspike.modules</groupId> <artifactId>deltaspike-data-module-api</artifactId> <version>1.7.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.apache.deltaspike.modules</groupId> <artifactId>deltaspike-data-module-impl</artifactId> <version>1.7.1</version> <scope>runtime</scope> </dependency>
Scalaを使うのは、ご愛嬌…。
Scalaを使うので、Scalaのプラグインも足しておきます。
※本質的ではありませんが
<plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <version>3.2.2</version> <executions> <execution> <goals> <goal>compile</goal> <goal>testCompile</goal> </goals> </execution> </executions> <configuration> <scalaVersion>${scala.version}</scalaVersion> <args> <arg>-Xlint</arg> <arg>-unchecked</arg> <arg>-deprecation</arg> <arg>-feature</arg> </args> <recompileMode>incremental</recompileMode> </configuration> </plugin>
beans.xmlも用意します。
src/main/resources/META-INF/beans.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="annotated"> </beans>
Entityを作成する
JPAで使うEntityを作成します。お題は、書籍とします。
src/main/scala/org/littlewings/javaee7/entity/Book.scala
package org.littlewings.javaee7.entity import javax.persistence.{Column, Entity, Id, Table} import scala.beans.BeanProperty object Book { def apply(isbn: String, title: String, price: Int): Book = { val book = new Book book.isbn = isbn book.title = title book.price = price book } } @Entity @Table(name = "book") @SerialVersionUID(1L) class Book extends Serializable { @Id @BeanProperty var isbn: String = _ @Column @BeanProperty var title: String = _ @Column @BeanProperty var price: Int = _ }
Data Moduleを使う
Data Moduleの使い方の基本は、@Repositoryアノテーションの付与とEntityRepositoryインターフェースを実装したインターフェースの作成です。
※Scalaなのでトレイトになっていますが
src/main/scala/org/littlewings/javaee7/repository/BookRepository.scala
package org.littlewings.javaee7.repository import org.apache.deltaspike.data.api.{EntityRepository, Query, Repository} import org.littlewings.javaee7.entity.Book @Repository trait BookRepository extends EntityRepository[Book, String]
これだけで、基本的な操作(save/remove/findBy(PrimaryKey)/findAll/count)などはできるようになります。
実装自体は、Partial-Beanという仕組みで生成しているようです。
メソッドの命名規則によるクエリの生成も可能です。
def findByPriceGreaterThanEqualsOrderByPriceDesc(price: Int): java.util.List[Book]
メソッドの実装自体は、行う必要はありません。
使える命名規則としては、以下のようになっています。
- Equal
- NotEqual
- Like
- GreaterThan
- GreaterThanEquals
- LessThan
- LessThanEquals
- Between
- IsNull
- IsNotNull
ORDER BYやLIMITといったことも可能です。
それでも表現できない場合は、@Queryアノテーションを使用してクエリを書きます。
Query Annotations
例えば、こんな感じで。
@Query("SELECT COUNT(b) FROM Book b WHERE b.price >= ?1") def countPriceGreaterThan(price: Int): Long @Query("SELECT b FROM Book b WHERE b.title LIKE ?1 ORDER BY b.price DESC") def findByTitleLike(title: String): java.util.List[Book]
パラメータは、「?N」なインデックス形式で指定するんですねぇ。こちらもメソッドの実装自体は、行う必要はありません。
QueryResultを使用することで、オプションやページングの指定ができたり
OptionalやStreamも使える模様。
細かくは書ききれないので、興味のある方はドキュメントを…。
というわけで、今回はこんなRepositoryを作成しました。
src/main/scala/org/littlewings/javaee7/repository/BookRepository.scala
package org.littlewings.javaee7.repository import org.apache.deltaspike.data.api.{EntityRepository, Query, Repository} import org.littlewings.javaee7.entity.Book @Repository trait BookRepository extends EntityRepository[Book, String] { def findByPriceGreaterThanEqualsOrderByPriceDesc(price: Int): java.util.List[Book] @Query("SELECT COUNT(b) FROM Book b WHERE b.price >= ?1") def countPriceGreaterThan(price: Int): Long @Query("SELECT b FROM Book b WHERE b.title LIKE ?1 ORDER BY b.price DESC") def findByTitleLike(title: String): java.util.List[Book] }
Test-Controlで単体テストする
それでは、動作確認を兼ねてApache DeltaSpikeのTest-Controlでテストしてみましょう。
準備
まずは、Test-Control用のモジュールおよびCDIコンテナ制御の依存関係、JPA実装、テストライブラリが必要なので追加します。データベースは、H2とします。
JPAの実装はHibernateですが、このあとWildFlyで使う関係上、WildFly 10.0.0.Finalに含まれているHibernateと同じバージョンを使用しています。
<!-- Unit Test --> <!-- Apache DeltaSpike Test-Control Module --> <dependency> <groupId>org.apache.deltaspike.modules</groupId> <artifactId>deltaspike-test-control-module-api</artifactId> <version>1.7.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.deltaspike.modules</groupId> <artifactId>deltaspike-test-control-module-impl</artifactId> <version>1.7.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.deltaspike.cdictrl</groupId> <artifactId>deltaspike-cdictrl-weld</artifactId> <version>1.7.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.weld.se</groupId> <artifactId>weld-se-core</artifactId> <version>2.3.5.Final</version> <scope>test</scope> </dependency> <!-- JPA Implementation for UnitTest --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>5.0.7.Final</version> <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.192</version> <scope>test</scope> </dependency> <!-- UnitTest Library --> <dependency> <groupId>org.scalatest</groupId> <artifactId>scalatest_${scala.major.version}</artifactId> <version>3.0.0</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency>
※「${scala.major.version}」は、「2.11」です
persistence.xml
先ほどのコード例では、Repositoryやエンティティは書きましたが、永続化ユニットの設定は書いていません。
単体テスト向けということで、こんなpersistence.xmlを用意。
src/main/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="unit-test.persistence.unit" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <properties> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/> <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.hbm2ddl.auto" value="update"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> </properties> </persistence-unit> </persistence>
EntityManagerのProducerを作成する
続いて、EntityManagerをCDI管理BeanとするためのProducerを作成します。こちらは、ProjectStageがUnitTestで有効となるように実装しました。
src/main/scala/org/littlewings/javaee7/producer/EntityManagerProducers.scala
package org.littlewings.javaee7.producer import javax.enterprise.context.{ApplicationScoped, Dependent} import javax.enterprise.inject.Produces import javax.persistence.{EntityManager, EntityManagerFactory, Persistence, PersistenceContext} import org.apache.deltaspike.core.api.exclude.Exclude import org.apache.deltaspike.core.api.projectstage.ProjectStage object EntityManagerProducers { @Dependent @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.UnitTest])) class UnitTestEntityManagerProducer { @Produces @ApplicationScoped def entityManagerFactoryProducer: EntityManagerFactory = Persistence.createEntityManagerFactory("unit-test.persistence.unit") @Produces def entityManagerProducer(emf: EntityManagerFactory): EntityManager = emf.createEntityManager } }
※あとでハマったのですが、@Excludeは@Producesに付けるのでは効かないことがわかり、こういう形になりました
テストコード
では、テストコードを書きます。
ざっくり、こんな感じで確認。
src/test/scala/org/littlewings/javaee7/repository/DeltaSpikeRepositoryTest.scala
package org.littlewings.javaee7.repository import javax.inject.Inject import javax.persistence.EntityManager import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner import org.junit.runner.RunWith import org.junit.{Before, Test} import org.littlewings.javaee7.entity.Book import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[CdiTestRunner]) class DeltaSpikeRepositoryTest extends JUnitSuite with Matchers { @Inject var bookRepository: BookRepository = _ @Inject var em: EntityManager = _ @Before def setUp(): Unit = { val tx = em.getTransaction tx.begin() em.createNativeQuery("TRUNCATE TABLE book").executeUpdate() tx.commit() } @Test def save(): Unit = { bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 3456)) bookRepository.save(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104)) bookRepository.save(Book("978-4798124605", "Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)", 4536)) bookRepository.count should be(3L) } @Test def findByPrimaryKey(): Unit = { bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 3456)) bookRepository.findBy("978-4774183169").price should be(3456) } @Test def update(): Unit = { val tx = em.getTransaction tx.begin() bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 3456)) tx.commit() bookRepository.findBy("978-4774183169").price should be(3456) val tx2 = em.getTransaction tx2.begin() bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 4456)) tx2.commit() bookRepository.findBy("978-4774183169").price should be(4456) } @Test def usingMethodExpressions(): Unit = { bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 3456)) bookRepository.save(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104)) bookRepository.save(Book("978-4798124605", "Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)", 4536)) val resultBooks = bookRepository.findByPriceGreaterThanEqualsOrderByPriceDesc(4000) resultBooks should have size (2) resultBooks.get(0).isbn should be("978-4798124605") // Begging Java EE 6 resultBooks.get(1).isbn should be("978-4798140926") // Java EE 7 } @Test def usingQueryAnnotation(): Unit = { bookRepository.save(Book("978-4774183169", "パーフェクト Java EE", 3456)) bookRepository.save(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104)) bookRepository.save(Book("978-4798124605", "Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)", 4536)) bookRepository.countPriceGreaterThan(4000) should be(2L) val resultBooks = bookRepository.findByTitleLike("%GlassFish%") resultBooks should have size (1) resultBooks.get(0).isbn should be("978-4798124605") } }
テストの内容については、そう奇抜なことはしていないので、省略…。
実行は、「mvn test」で。
Arquillianを使ってインテグレーションテスト
では、最後にArquillianを使ってアプリケーションサーバー(ここではWildFly)にデプロイして確認してみます。
Java EE環境でやるということで、トランザクション管理はJTAの@Transactionalを使って管理したいと思います。
Apache DeltaSpikeにも@Transactionalというアノテーションがあるのですが、今回はこちらは置いておきます。
JPA Module
という条件を満たす実装、テストコードを書いてみましたよ、というお話です。
準備
ArquillianをWildFly Remoteで使うということと、デプロイの関係上Shrinkwrapが必要です。
Dependency Managementに、bomを追加。
<dependencyManagement> <dependencies> <dependency> <groupId>org.jboss.arquillian</groupId> <artifactId>arquillian-bom</artifactId> <version>1.1.11.Final</version> <scope>import</scope> <type>pom</type> </dependency> <dependency> <groupId>org.jboss.shrinkwrap</groupId> <artifactId>shrinkwrap-depchain</artifactId> <version>1.2.6</version> <scope>import</scope> <type>pom</type> </dependency> </dependencies> </dependencyManagement>
あとは依存関係にArquillianとWildFly Remote、ShrinkwrapのMaven Resolverを足していきます。
<!-- Arquillian Integration Test --> <dependency> <groupId>org.jboss.arquillian.junit</groupId> <artifactId>arquillian-junit-container</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.wildfly.arquillian</groupId> <artifactId>wildfly-arquillian-container-remote</artifactId> <version>2.0.0.Final</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.shrinkwrap.resolver</groupId> <artifactId>shrinkwrap-resolver-api-maven</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.shrinkwrap.resolver</groupId> <artifactId>shrinkwrap-resolver-spi-maven</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.shrinkwrap.resolver</groupId> <artifactId>shrinkwrap-resolver-impl-maven</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.shrinkwrap.resolver</groupId> <artifactId>shrinkwrap-resolver-impl-maven-archive</artifactId> <scope>test</scope> </dependency>
参考)
Creating Deployable Archives with ShrinkWrap · Arquillian Guides
GitHub - shrinkwrap/resolver: ShrinkWrap Resolvers
インテグレーションテスト用のソースコードおよび設定は、「src/integration-test」配下に置くことにして、テスト対象は「**/*IT」とします。
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.12</version> <executions> <execution> <id>add-test-source</id> <phase>generate-test-sources</phase> <goals> <goal>add-test-source</goal> </goals> <configuration> <sources> <source>src/integration-test/scala</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.19.1</version> <executions> <execution> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> <configuration> <includes> <include>**/*IT</include> </includes> </configuration> </plugin>
Arquillianの設定としては、今回はServlet 3.0のみを指定しておきます。
src/integration-test/resources/aqruillian.xml
<?xml version="1.0" encoding="UTF-8"?> <arquillian xmlns="http://jboss.org/schema/arquillian" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd"> <defaultProtocol type="Servlet 3.0"/> </arquillian>
またWildFlyをダウンロード、展開し、ProjectStageをIntegrationTestとして起動しておきます。
$ wildfly-10.0.0.Final/bin/standalone.sh -Dorg.apache.deltaspike.ProjectStage=IntegrationTest
Serviceクラスの実装
@Transactionalを使うということで、先ほど作成したRepositoryを使ったServiceクラスを作成します。
src/main/scala/org/littlewings/javaee7/service/BookService.scala
package org.littlewings.javaee7.service import javax.enterprise.context.ApplicationScoped import javax.inject.Inject import javax.transaction.Transactional import org.littlewings.javaee7.entity.Book import org.littlewings.javaee7.repository.BookRepository @ApplicationScoped class BookService { @Inject var bookRepository: BookRepository = _ @Transactional def save(book: Book): Book = bookRepository.save(book) @Transactional def saveFail(book: Book): Book = { save(book) throw new RuntimeException("Oops!!") } @Transactional def findByIsbn(isbn: String): Book = bookRepository.findBy(isbn) @Transactional def countPriceGreaterThan(price: Int): Long = bookRepository.countPriceGreaterThan(price) @Transactional def findByTitleLike(title: String): java.util.List[Book] = bookRepository.findByTitleLike(title) }
ロールバックを確認するためのメソッドも入れています。
DataSourceとpersistence.xmlとEntityManagerのProducer
インテグレーションテストでは、単体テストで使っていたH2はやめて、MySQLで確認することにします。
データベースについては、MySQL側に事前にこんなテーブルを作っておきます。
CREATE TABLE IF NOT EXISTS book( isbn VARCHAR(14), title VARCHAR(200), price INT(10), PRIMARY KEY(isbn) );
データソースは、こんなスクリプトでJDBCドライバのデプロイとともに、一気に作成。JDBCドライバは、MySQLのドライバがあらかじめMavenローカルリポジトリにあるものとします。
src/integration-test/resources/jboss-deploy-and-create-ds-script
connect deploy ~/.m2/repository/mysql/mysql-connector-java/6.0.3/mysql-connector-java-6.0.3.jar data-source add --name=mysqlDs --driver-name=mysql-connector-java-6.0.3.jar --driver-class=com.mysql.cj.jdbc.Driver --jndi-name=java:jboss/datasources/jdbc/mysqlDs --jta=true --connection-url=jdbc:mysql://localhost:3306/test --user-name=kazuhira --password=password cd /subsystem=datasources/data-source=mysqlDs ./connection-properties=useUnicode:add(value=true) ./connection-properties=characterEncoding:add(value=utf-8) ./connection-properties=characterSetResults:add(value=utf-8) ./connection-properties=useServerPrepStmts:add(value=true) ./connection-properties=useLocalSessionState:add(value=true) ./connection-properties=elideSetAutoCommits:add(value=true) ./connection-properties=alwaysSendSetIsolation:add(value=false) ./connection-properties=useSSL:add(value=false) reload
実行。
$ /path/to/wildfly-10.0.0.Final/bin/jboss-cli.sh --file=src/integration-test/resources/jboss-deploy-and-create-ds-script
これで、データソースの作成まで完了です。
このデータソースをpersistence.xmlの永続化ユニットとして使うように定義し
<persistence-unit name="integration-test.persistence.unit" transaction-type="JTA"> <jta-data-source>java:jboss/datasources/jdbc/mysqlDs</jta-data-source> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL57InnoDBDialect"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.hbm2ddl.auto" value="validate"/> </properties> </persistence-unit>
EntityManagerのProducer側にも追加します。
object EntityManagerProducers { @Dependent @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.IntegrationTest])) class IntegrationTestEntityManagerProducer { @PersistenceContext(unitName = "integration-test.persistence.unit") var entityManager: EntityManager = _ @Produces @ApplicationScoped def entityManagerProducer: EntityManager = entityManager } @Dependent @Exclude(exceptIfProjectStage = Array(classOf[ProjectStage.UnitTest])) class UnitTestEntityManagerProducer { // 省略 } }
テストを書いて実行
では、これでArquillianを使ったテストを書いていきます。
src/integration-test/scala/org/littlewings/javaee7/service/ArquillianWithDeltaSpikeServiceIT.scala
package org.littlewings.javaee7.service import javax.inject.Inject import javax.persistence.EntityManager import javax.transaction.UserTransaction import org.jboss.arquillian.container.test.api.Deployment import org.jboss.arquillian.junit.Arquillian import org.jboss.shrinkwrap.api.ShrinkWrap import org.jboss.shrinkwrap.api.spec.WebArchive import org.jboss.shrinkwrap.resolver.api.maven.Maven import org.junit.runner.RunWith import org.junit.{Before, Test} import org.littlewings.javaee7.entity.Book import org.scalatest.Matchers import org.scalatest.junit.JUnitSuite @RunWith(classOf[Arquillian]) class ArquillianWithDeltaSpikeServiceIT extends JUnitSuite with Matchers { @Inject var userTransaction: UserTransaction = _ @Inject var em: EntityManager = _ @Inject var bookService: BookService = _ @Before def setUp(): Unit = { userTransaction.begin() em.createNativeQuery("TRUNCATE TABLE book").executeUpdate() userTransaction.commit() } @Test def save(): Unit = { bookService.save(Book("978-4774183169", "パーフェクト Java EE", 3456)) bookService.save(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104)) bookService.save(Book("978-4798124605", "Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)", 4536)) bookService.findByIsbn("978-4774183169").price should be(3456) } @Test def rollback(): Unit = { val thrown = the[RuntimeException] thrownBy bookService.saveFail(Book("978-4774183169", "パーフェクト Java EE", 3456)) thrown.getMessage should be("Oops!!") bookService.findByIsbn("978-4774183169") should be(null) } @Test def usingQueryAnnotation(): Unit = { bookService.save(Book("978-4774183169", "パーフェクト Java EE", 3456)) bookService.save(Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104)) bookService.save(Book("978-4798124605", "Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)", 4536)) val resultBooks = bookService.findByTitleLike("%徹底入門%") resultBooks should have size(1) resultBooks.get(0).isbn should be("978-4798140926") } } object ArquillianWithDeltaSpikeServiceIT { @Deployment def createDeployment: WebArchive = { ShrinkWrap .create(classOf[WebArchive]) .addPackages(true, "org.littlewings.javaee7") .addAsResource("META-INF/apache-deltaspike.properties", "META-INF/apache-deltaspike.properties") .addAsResource("META-INF/beans.xml", "META-INF/beans.xml") .addAsResource("META-INF/persistence.xml", "META-INF/persistence.xml") .addAsLibraries( Maven .resolver .loadPomFromFile("pom.xml") .importRuntimeDependencies .resolve("org.scalatest:scalatest_2.11:3.0.0") .withTransitivity .asFile: _* ) } }
ではこれで、テストを実行。
$ mvn test-compile failsafe:integration-test
※単体テストのコードは動かしたくなかったので、この指定…
ところが、これはうまくいきません。
このコードをそのまま動かすと、以下のようなエラーを見ることになります。
java.lang.IllegalStateException: A JTA EntityManager cannot use getTransaction()
これは、Apache DeltaSpikeのData Moduleが依存する、JPA ModuleがRESOURCE_LOCALを想定していることが理由です。
参考)
DeltaSpike DataをJava EEアプリケーションサーバー上で動かす(2) | なるほど!ザ・Weld
これを回避するためには、META-INF/apache-deltaspile.propertiesに以下のような記述をします。
src/main/resources/META-INF/apache-deltaspike.properties
globalAlternatives.org.apache.deltaspike.jpa.spi.transaction.TransactionStrategy=org.apache.deltaspike.jpa.impl.transaction.ContainerManagedTransactionStrategy
コンテナ管理のトランザクション制御を利用するようにしましょう、と。TransactionStrategyインターフェースの実装で選択するのですが、以下のパッケージに実装があります。
https://github.com/apache/deltaspike/tree/deltaspike-1.7.1/deltaspike/modules/jpa/impl/src/main/java/org/apache/deltaspike/jpa/impl/transaction
デフォルトはResourceLocalTransactionStrategyで、その他にBeanManagedUserTransactionStrategy、ContainerManagedTransactionStrategy、EnvironmentAwareTransactionStrategyがありますが、今回の目的で使えるのはContainerManagedTransactionStrategyだけです。
また、ドキュメントの方には、beans.xmlにalternativesを書けばいいよと書かれていましたが、うまく動作しませんでした…。
この設定を入れると、テストをパスするようになります。
めでたし、めでたし。
単体テストはどうした
ですが、ここで「src/main/resources/META-INF/apache-deltaspike.properties」にTransactionStrategyの設定を書いてしまったので、実は単体テスト側の動作を破壊したことになります。
さて、どうしましょう?
「src/main/resources/META-INF/apache-deltaspike.properties」の内容は変えないとして、単体テストではResourceLocalTransactionStrategyを使いたいわけです。また、globalAlternativesについてはProjectStageは見てくれません。
今回は、別の設定ファイルを追加して、apache-deltaspike.propertiesより優先度の高いファイルを作り、その内容を上書きする方法で対処しました。
次のようなクラスを作成します。追加するファイルは、「unit-test-apache-deltaspike.properties」とします。
src/test/scala/org/littlewings/javaee7/repository/UnitTestApacheDeltaSpikePropertyFileConfig.scala
package org.littlewings.javaee7.repository import org.apache.deltaspike.core.api.config.PropertyFileConfig class UnitTestApacheDeltaSpikePropertyFileConfig extends PropertyFileConfig { override def getPropertyFileName: String = "META-INF/unit-test-apache-deltaspike.properties" override def isOptional: Boolean = false }
このクラスをService Providerの仕組みに乗せます。
src/test/resources/META-INF/services/org.apache.deltaspike.core.api.config.PropertyFileConfig
org.littlewings.javaee7.repository.UnitTestApacheDeltaSpikePropertyFileConfig
「unit-test-apache-deltaspike.properties」の内容は、以下のように書きます。
src/test/resources/META-INF/unit-test-apache-deltaspike.properties
deltaspike_ordinal=110 globalAlternatives.org.apache.deltaspike.jpa.spi.transaction.TransactionStrategy=org.apache.deltaspike.jpa.impl.transaction.ResourceLocalTransactionStrategy
TransactionStrategyをResourceLocalTransactionStrategyにするのはもちろんですが、deltaspike_ordinalを100よりも大きくすることで、オリジナルのapache-deltaspile.propertiesよりも優先度を上にします。
Providing configuration using ConfigSources
Propertiesファイルの優先度は100で、値が大きいほど優先度が高くなります。システムプロパティが最高で、400になっています。
今回は、これで単体テストではResourceLocalTransactionStrategyを使い、インテグレーションテストではContainerManagedTransactionStrategyを使うようにしました。
まとめ
Apache DeltaSpikeのData Moduleを使い、JPAのクエリ実行を試しつつ、Test-Controlで単体テスト、ArquillianでインテグレーションテストをしてJTAとの統合まで確認してみました。
Data Module自体より、ProjectStageとTransactionStrategyにとてもハマったのですが、なんとか目標にしていた形には持っていけました。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/cdi-jpa-deltaspike-data
参考)
DeltaSpike Dataの紹介 | なるほど!ザ・Weld
DeltaSpike DataをJava EEアプリケーションサーバー上で動かす(1) | なるほど!ザ・Weld
DeltaSpike DataをJava EEアプリケーションサーバー上で動かす(2) | なるほど!ザ・Weld
Creating Deployable Archives with ShrinkWrap · Arquillian Guides
GitHub - shrinkwrap/resolver: ShrinkWrap Resolvers
build - Prevent unit tests in maven but allow integration tests - Stack Overflow