【注意事項あり】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、IDEIntelliJ IDEA 14.1.5です。

CDI/EJBJTAについて

これ以降の内容は、CDI/EJBJTAの知識が前提となります。下記の資料が参考になります。

JavaDayTokyo2015での寺田さんの発表資料です。CDIについて詳しくまとまっています。

http://www.oracle.co.jp/jdt2015/pdf/2-2.pdf

@opengl-8080さんのJTAの解説ブログです。いつもながら勉強になります。

JavaEE使い方メモ(JTA) - Qiita

アノテーションをつける答えは@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です)にします。アプリケーションに関する設定情報を保持するクラスなので、スコープはアプリケーションスコープが適切と思われます。

DataSourceDialectについては、別途プロデューサークラスを作成して、@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です。

訂正とお詫び:@ResourceJDBCリソースを取得する際、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にこの機能が入った経緯のブログ記事です。

DomaのEJB3.1対応 - taediumの日記