【注意事項あり】Doma 2だけどCDI/EJB使ってJTAでトランザクション管理したい!そしてJAX-RSでREST作りたい!
Doma 2とは?基本的な使い方は?
Doma 2は、SQLを外部ファイルに書くことができるORマッパーです。ネイティブSQLが書けること、依存ライブラリが無い事、国産 OSSで日本語ドキュメントが充実している事などが魅力です。
下記の記事もご参考になさってください。2014年のJavaアドベントカレンダー向けに書いたものです。
美しき青きDoma!~SQLとIDEが奏でる美しきORマッピング~ - Java EE 事始め!
Domaでは、プログラマが作るのはHogeDaoインターフェイスだけで、その実装クラスHogeDaoImplはGradleでビルド時にAnnotation Processorで自動生成されます。
CDI/EJB使ってHogeDao型のフィールドにHogeDaoImplのインスタンスをインジェクションしたり、JTAでトランザクション管理するには、実装クラス側(HogeDaoImpl)に@RequestScoped
(CDI)や@Stateless
(EJB)などのアノテーションを付加する必要があります。
しかし、実装クラスはビルド時に生成されるし、生成後に手作業でアノテーションを付けても、再ビルド時に上書きされて消えてしまいます。
アノテーションをつける方法がずっと分からなかったのですが、遂にやり方が分かったのでご紹介します。
ソースはGitHubに公開しています。
MasatoshiTada/doma-jaxrs · GitHub
APサーバーはPayara Web 4.1.153ですが、Jerseyには依存しないように書いたので、WildFlyでもそのまま動作します(9.0.1.Finalで確認済み)。DBはMySQL 5.6、IDEはIntelliJ IDEA 14.1.5です。
CDI/EJBとJTAについて
これ以降の内容は、CDI/EJBとJTAの知識が前提となります。下記の資料が参考になります。
JavaDayTokyo2015での寺田さんの発表資料です。CDIについて詳しくまとまっています。
http://www.oracle.co.jp/jdt2015/pdf/2-2.pdf
@opengl-8080さんのJTAの解説ブログです。いつもながら勉強になります。
アノテーションをつける答えは@AnnnotateWith
ツイッターである方々のやり取りを見ていたら、Doma 2の公式ドキュメントの一部が目に入りました。
http://doma.readthedocs.org/ja/stable/config/#id22
@AnnnotateWith
というアノテーションを使っていますね。これがポイントです。
1.アノテーションの自作
まず、こんなアノテーションを自作します。自作するアノテーション名は任意ですが、公式ドキュメントに従って@InjectConfig
という名前にしておきます。
CDIの場合
package com.example.dao.config; import org.seasar.doma.AnnotateWith; import org.seasar.doma.Annotation; import org.seasar.doma.AnnotationTarget; import javax.enterprise.context.Dependent; import javax.inject.Inject; @AnnotateWith(annotations = { @Annotation(target = AnnotationTarget.CLASS, type = Dependent.class) , @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class) }) public @interface InjectConfig { }
EJBの場合
package com.example.dao.config; import org.seasar.doma.AnnotateWith; import org.seasar.doma.Annotation; import org.seasar.doma.AnnotationTarget; import javax.ejb.Stateless; import javax.inject.Inject; @AnnotateWith(annotations = { @Annotation(target = AnnotationTarget.CLASS, type = Stateless.class) , @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class) }) public @interface InjectConfig { }
注意!! Payara 4.1.1.154までのAPサーバーでは、CDI+JPA以外のORマッパー(というよりJDBC)では、JTAが正しく動作しない可能性があることが判明しました。なので、現段階ではEJBを使ってください。このことについては、PayaraのGitHubにIssueとして報告済みです。次バージョンでの改善を期待しています。WildFlyでは、CDIでもEJBでも正しく動作します。 参考URL @OpenGL_8080さんのブログ(コメント欄をご確認ください) JavaEE使い方メモ(JTA) - Qiita PayaraのGitHub Payara does NOT rollback when RuntimeException occurs in CDI @Transactional method using JDBC · Issue #505 · payara/Payara · GitHub
@Annotation
のtype属性はアノテーション名.class、target属性はtypeで指定したアノテーションを付加する対象を表します。
この例だと、クラスに@Dependent
、コンストラクタに@Inject
を付けるということになります。
2. 自作アノテーションをDaoインターフェイスに付加
次に、自作した@InjectConfing
アノテーションを、自作Daoインターフェイスに付加します。
package com.example.dao; import com.example.dao.config.InjectConfig; import com.example.entity.Employee; import org.seasar.doma.Dao; import org.seasar.doma.Script; import org.seasar.doma.Select; import java.util.List; import java.util.Optional; @Dao @InjectConfig public interface EmployeeDao { @Script void create(); @Select Optional<Employee> selectById(Integer empId); @Select List<Employee> selectLikeName(String name); }
3.Gradleでビルド
Gradleでビルドすると、EmployeeDao
インターフェイスの実装クラスが生成されます。
CDIの場合
package com.example.dao; /** */ @javax.enterprise.context.Dependent() @javax.annotation.Generated(value = { "Doma", "2.5.0" }, date = "2015-10-15T20:48:55.929+0900") public class EmployeeDaoImpl extends org.seasar.doma.internal.jdbc.dao.AbstractDao implements com.example.dao.EmployeeDao { /** * @param config the config */ @javax.inject.Inject() public EmployeeDaoImpl(org.seasar.doma.jdbc.Config config) { super(config); }
EJBの場合
package com.example.dao; /** */ @javax.ejb.Stateless() @javax.annotation.Generated(value = { "Doma", "2.5.0" }, date = "2015-10-15T20:48:55.929+0900") public class EmployeeDaoImpl extends org.seasar.doma.internal.jdbc.dao.AbstractDao implements com.example.dao.EmployeeDao { /** * @param config the config */ @javax.inject.Inject() public EmployeeDaoImpl(org.seasar.doma.jdbc.Config config) { super(config); }
@AnnnotateWith
で指定した通り、クラスに@Dependent
または@Stateless
、コンストラクタに@Inject
が付加されています。これで、このEmployeeDaoImpl
を他のクラスにCDIでインジェクトできるようになります。
また、コンストラクタには@Inject
が付加されています。
引数のConfig
インターフェイスは、データソースなどを保持するもので、後ほど実装クラスを作成します。
Config
実装クラスを作成して@ApplicationScoped
などを付加しておけば、この実装クラスのインスタンスが、コンストラクタインジェクションされます。
4. Config
実装クラスの作成
package com.example.dao.config; import org.seasar.doma.jdbc.Config; import org.seasar.doma.jdbc.dialect.Dialect; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import javax.sql.DataSource; @ApplicationScoped public class AppConfig implements Config { @Inject private DataSource dataSource; @Inject private Dialect dialect; @Override public DataSource getDataSource() { return dataSource; } @Override public Dialect getDialect() { return dialect; } }
@ApplicationScoped
を付加してCDI管理ビーン(このクラスはEJBでなくでOKです)にします。アプリケーションに関する設定情報を保持するクラスなので、スコープはアプリケーションスコープが適切と思われます。
DataSource
とDialect
については、別途プロデューサークラスを作成して、@Inject
で取得できるようにしています。
package com.example.dao.config; import org.seasar.doma.jdbc.dialect.Dialect; import org.seasar.doma.jdbc.dialect.MysqlDialect; import javax.annotation.Resource; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; import javax.sql.DataSource; import java.io.Serializable; @Dependent public class DataSourceProducer implements Serializable { @Resource(lookup = "jdbc/sandbox") private DataSource dataSource; private Dialect dialect = new MysqlDialect(); @Produces public DataSource getDataSource() { return dataSource; } @Produces public Dialect getDialect() { return dialect; } }
このクラスもEJBでなくてOKです。
訂正とお詫び:@Resource
でJDBCリソースを取得する際、name
属性を使っていたのですが、正しくはlookup
属性でした。訂正してお詫びします。
Config
実装クラスが複数ある場合は?
データソースが複数ある場合など、Config
実装クラスが複数になる可能性があると思います。その場合はQualifierを自作して、それをConfig
実装クラスと@AnnotatedWith
に追加すれば良いはずです。
@AnnotateWith(annotations = { @Annotation(target = AnnotationTarget.CLASS, type = Dependent.class) , @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class) , @Annotation(target = AnnotationTarget.CONSTRUCTOR_PARAMETER, type = 自作Qualifier1.class }) public @interface InjectConfig1 { }
@ApplicationScoped @自作Qualifier1 public class AppConfig1 implements Config { // 省略 }
@AnnotateWith(annotations = { @Annotation(target = AnnotationTarget.CLASS, type = Dependent.class) , @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class) , @Annotation(target = AnnotationTarget.CONSTRUCTOR_PARAMETER, type = 自作Qualifier2.class }) public @interface InjectConfig2 { }
@ApplicationScoped @自作Qualifier2 public class AppConfig2 implements Config { // 省略 }
JTAでトランザクション管理する
DataSource
は@Resource
で取ってきているし、DaoImplクラスはCDI管理にできたので、後は簡単です。
CDIの場合
package com.example.service; import com.example.dao.EmployeeDao; import com.example.entity.Employee; import com.example.resource.dto.EmployeeDto; import javax.enterprise.context.Dependent; import javax.inject.Inject; import javax.transaction.Transactional; import java.io.Serializable; import java.util.Optional; @Dependent public class EmployeeService implements Serializable { @Inject private EmployeeDao employeeDao; @Transactional(Transactional.TxType.REQUIRED) public Optional<EmployeeDto> selectById(Integer empId) { Optional<Employee> employeeOptional = employeeDao.selectById(empId); return employeeOptional.map(this::convertToDto); }
EJBの場合
package com.example.service; import com.example.dao.EmployeeDao; import com.example.entity.Employee; import com.example.resource.dto.EmployeeDto; import javax.ejb.Stateless; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import javax.inject.Inject; import java.io.Serializable; import java.util.Optional; @Stateless public class EmployeeService implements Serializable { @Inject private EmployeeDao employeeDao; @TransactionAttribute(TransactionAttributeType.REQUIRED) public Optional<EmployeeDto> selectById(Integer empId) { Optional<Employee> employeeOptional = employeeDao.selectById(empId); return employeeOptional.map(this::convertToDto); }
@Inject
でインジェクトすると、EmployeeDaoImpl
のインスタンスが注入されます。で、メソッドに@Transactional
を付加すればOK!
JAX-RSでRESTを作る
package com.example.resource; import com.example.resource.dto.EmployeeDto; import com.example.service.EmployeeService; import org.hibernate.validator.constraints.NotBlank; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.validation.constraints.Pattern; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @Path("employees") @RequestScoped public class EmployeeResource { @Inject private EmployeeService employeeService; @GET @Path("{empId}") @Produces(MediaType.APPLICATION_JSON) public Response selectById(@PathParam("empId") @Pattern(regexp = "[1-9][0-9]*") String empIdStr) { Integer empId = Integer.valueOf(empIdStr); EmployeeDto employeeDto = employeeService.selectById(empId) .orElseThrow(() -> new NotFoundException("該当する社員が見つかりませんでした")); return Response.ok(employeeDto).build(); } }
感想
これで去年からの疑問点が解決しました。ようやっとスッキリです。
あとはDomaの使い方さえ知れれば、怖いものなし・・・のはず!
併せて読みたい
うらがみさんのブログ。Doma+JAX-RS連携について書かれています。
Thymeleaf + JAX-RS + DomaをGlassFishで試してみる - 裏紙
今回利用した、@siosioさん作のIntelliJ IDEA Doma Support Plugin
IntelliJ IDEA用のDomaプラグイン作ってみた - しおしお
Doma公式ドキュメント
Welcome to Doma — Doma 2.0 ドキュメント
Domaにこの機能が入った経緯のブログ記事です。