Gradle で始める Payara 5 〜 CDI・JPA 〜

f:id:Naotsugu:20201229231421p:plain


はじめに

前回作成した、

blog1.mammb.com

に続き、データベース処理を追加してみましょう。


データソースの登録

データソースの登録は、JavaEE 6 から標準化されました。 application.xml, web.xml, ejb-jar.xml といったデプロイメント記述子に <data-source> 要素で DataSource リソースを定義できます。

以下のような定義を行うことで、アプリケーションサーバに依存せずにデータソースを構成することができます。

<data-source>
  <name>java:app/MyApp/MyDS</name>
  <class-name>org.h2.jdbcx.JdbcDataSource</class-name>
  <url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</url>
  <user>sa</user>
  <password></password>
</data-source>


また、上記データソースの登録は @DataSourceDefinition というアノテーションベースでの構成も可能です。


ここでは @DataSourceDefinition を使ってデータソースを構成します。

war モジュールの中に、DataSourceInitializer というクラスを以下のように作成します。

package javaee8.starter;

import javax.annotation.sql.DataSourceDefinition;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;

@DataSourceDefinition(
        name = DataSourceInitializer.DS_NAME,
        className = "org.h2.jdbcx.JdbcDataSource",
        initialPoolSize = 3,
        minPoolSize = 3,
        maxPoolSize = 100,
        url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
        properties = {
                "fish.payara.is-connection-validation-required=true",
                "fish.payara.connection-validation-method=custom-validation",
                "fish.payara.validation-classname=org.glassfish.api.jdbc.validation.H2ConnectionValidation",
                "fish.payara.connection-leak-timeout-in-seconds=5",
                "fish.payara.statement-leak-timeout-in-seconds=5"
        })
@Stateless
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public class DataSourceInitializer {
    public static final String DS_NAME = "java:app/MyApp/MyDS";
}

データベースは h2 をインメモリで使います。Payara には h2 が含まれるため、ライブラリを依存に追加する必要はありません。


上記は @Stateless により SSB としていますが、@Startup@Singleton としてデータベースの初期化処理を行うこともできます。

例えば以下のようになります。

// ...
@Startup
@Singleton
public class DataSourceInitializer {
    // ...
    @PostConstruct
    public void init() {
        try (Connection connection = dataSource.getConnection();
             Statement stmt = connection.createStatement()) {
            stmt.setQueryTimeout(10);
            boolean isValid = stmt.execute("SELECT '1'");
            if (!isValid) {
                throw new RuntimeException("connection validation failed.");
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

SELECT '1' で接続を検証する例となります。

@Startup@Singleton は EJB のアノテーションとなりますが、CDI でも同様な操作が可能です。


CDI の有効化

CDI 1.1 以降では、beans.xml を用意せずともCDIが有効になります。Bean定義アノテーションが付いたBean は CDI 管理対象となります。ただし javax.inject.Singleton は対象外など、注意点があります。以下を参考にしてください。

blog1.mammb.com

@Startup は CDI には存在しないため、@Observes で初期化イベントに応答することで同等の動きを得ることが出来ます。

以下のようになります。

// ...
@ApplicationScoped
public class DataSourceInitializer {
    // ...
    public void postConstruct(@Observes @Initialized(ApplicationScoped.class) Object o) {
        // ...
    }
}

EJB は CDI に取って代わられていくので、こちらの対応にしておいたほうが良いと思います。


JPA の導入

JPA を扱うためには、persistence.xml が必要になります。

以下のように作成します。

$ touch war/src/main/resources/META-INF/persistence.xml

war/src/main/resources/META-INF/persistence.xml を以下のように編集します。

<persistence version="2.2"
             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_2.xsd">
    <persistence-unit name="default">
        <jta-data-source>java:app/MyApp/MyDS</jta-data-source>
        <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
        <properties>
            <property name="javax.persistence.schema-generation.database.action" value="create"/>
            <property name="javax.persistence.schema-generation.scripts.action" value="drop-and-create"/>
            <property name="javax.persistence.schema-generation.scripts.create-target" value="./create.sql"/>
            <property name="javax.persistence.schema-generation.scripts.drop-target" value="./drop.sql"/>
            <property name="eclipselink.ddl-generation.index-foreign-keys" value="true"/>
            <property name="eclipselink.logging.level" value="INFO"/>
            <property name="eclipselink.logging.level.sql" value="FINE"/>
            <property name="eclipselink.logging.parameters" value="true"/>
            <property name="eclipselink.target-server" value="Glassfish" />
        </properties>
    </persistence-unit>
</persistence>

よく使うプロパティを入れておきました。


Entity の作成

簡単な Entity を以下のように作成します。

package javaee8.starter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.Email;
import java.io.Serializable;

@Entity
public class User implements Serializable {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @Email
    private String email;

    protected User() {
    }

    @JsonbCreator
    public User(@JsonbProperty("name") String name,
                @JsonbProperty("email") String email) {
        this.name = name;
        this.email = email;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
}

コンストラクタには @JsonbCreator でプロパティ定義を入れています。

JSON-B でデシリアライズを行う場合には、public なフィールドか、getter/setter が必要ですが、setter は使いたくないためコンストラクタで受付できるようにしています。


リポジトリの作成

リポジトリは EJB を使う場合には以下のように作成できます。

@Stateless
public class UserRepository {

    @PersistenceContext
    private EntityManager em;

}


しかしここでは、CDI で進めたいので、@Stateless に替えて @RequestScoped を使い、以下のようになります。

@RequestScoped
public class UserRepository {

    @PersistenceContext
    private EntityManager em;

}

@RequestScoped としているのは、EntityManager はスレッドセーフ性が保証されていないため、リクエストの都度新しいインスタンスを作成して、異なる EntityManager を取得するためです。

@Stateless の場合は、EJBインスタンスはプールされますが、1 つのスレッドに紐づくことをコンテナ側で保証しています。


EntityManager を生成する @Produces を用意することで対応することもできます。

以下のクラスを作成します。

package javaee8.starter;

import javax.enterprise.context.Dependent;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Produces;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Dependent
public class Producer {

    @PersistenceContext
    private EntityManager em;

    @Produces
    @RequestScoped
    private EntityManager produceEntityManager() {
        return em;
    }
}

@RequestScoped@ProducesEntityManager を生成します(メソッドは private でも大丈夫です)。

そうすれば、UserRepository は以下のように作成できます。


package javaee8.starter;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.util.List;

@ApplicationScoped
public class UserRepository {

    @Inject
    private EntityManager em;

    public User findById(Long id) {
        return em.find(User.class, id);
    }

    public List<User> findAll() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> user = query.from(User.class);
        query.select(user).orderBy(cb.asc(user.get(User_.name)));
        return em.createQuery(query).getResultList();
    }

}

@Inject により、プロデューサーによりリクエストスコープな EntityManager をインジェクトすることができます(実際にはリクエストスコープなプロキシ)。

リポジトリでは、クエリーのみを扱うようにしています。


サービスの作成

サービスは以下のようになります。

package javaee8.starter;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;

@RequestScoped
public class UserService {

    @Inject
    private EntityManager em;

    @Transactional
    public User register(User user) {
        em.persist(user);
        return user;
    }

}

リポジトリの場合と同様に、@ApplicationScoped とすることもできますが、アプリケーションのライフサイクル全体でインスタンスが存命するのも微妙なので、こちらは @RequestScoped としています。

もちろん EntityManager は先程と同様にプロデューサーにより @RequestScoped でインジェクトされるため、@ApplicationScoped とすることもできます。

登録メソッドは JTA 1.2で追加された @Transactional を付与しています。トランザクションタイプは、EJB と同様に Transactional.TxType.REQUIRED がデフォルト値となっています。


リソースの作成

UsersResource を以下のように作成します。

package javaee8.starter;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.util.List;

@Path("users")
public class UsersResource {

    @Inject
    private UserService userService;

    @Inject
    private UserRepository userRepository;

    @Context
    private UriInfo uriInfo;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<User> get() {
        return userRepository.findAll();
    }

    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public User get(@PathParam("id") Long id) {
        return userRepository.findById(id);
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response post(User user) {
        user = userService.register(user);
        UriBuilder builder = uriInfo.getAbsolutePathBuilder();
        builder.path(String.valueOf(user.getId()));
        return Response.created(builder.build()).build();
    }

}

とくに珍しいものはありません。


アプリケーションの実行

以下でアプリケーションを実行できます。

$ gradlew run


ユーザを登録します。

$ curl -X POST -H "Content-Type: application/json" -d '{"name":"thom", "email":"thom@example.com"}' --dump-header - localhost:8083/app/rs/users

HTTP/1.1 201 Created
Server: Undefined Product Name - define product and version info in config/branding  0.0.0
X-Powered-By: Servlet/4.0 JSP/2.3 (Undefined Product Name - define product and version info in config/branding  0.0.0  Java/AdoptOpenJDK/11)
Location: http://localhost:8083/app/rs/users/1
Content-Length: 0
X-Frame-Options: SAMEORIGIN

Location: http://localhost:8083/app/rs/users/1 で作成できました。


作成された対象を取得してみましょう。

$ curl localhost:8083/app/rs/users/1 | jq

{
  "email": "thom@example.com",
  "id": 1,
  "name": "thom"
}

作成できていますね。

なお、ここでは JSON の整形に jq を使っています。

入っていない場合は brew install jq などでインストールしておくと便利です。


さらに1件登録してみます。

$ curl -X POST -H "Content-Type: application/json" -d '{"name":"jonny", "email":"jonny@example.com"}' localhost:8083/app/rs/users


一覧を取得してみます。

$ curl localhost:8083/app/rs/users | jq

[
  {
    "email": "jonny@example.com",
    "id": 2,
    "name": "jonny"
  },
  {
    "email": "thom@example.com",
    "id": 1,
    "name": "thom"
  }
]

登録されていますね。


まとめ

JavaEE 8 をベースに、CDI と JPA による簡単なデータベース操作とトランザクション処理を見ました。

次回は、Java EE Security API 1.0 (JSR-375) と JSF 2.3(JSR-372) について見ていこうと思います。