はじめに
前回作成した、
に続き、データベース処理を追加してみましょう。
データソースの登録
データソースの登録は、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
は対象外など、注意点があります。以下を参考にしてください。
@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
な @Produces
で EntityManager
を生成します(メソッドは 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) について見ていこうと思います。