原文はこちら。
The original article was written by Tomáš Kraus (Senior Software Developer, Oracle).
https://medium.com/helidon/data-persistence-with-helidon-and-native-image-e5a74897ec6d
マイクロサービスはデータを保存しますが、これは問題ありません。ほとんどの場合、データは何らかのデータベースに保存されます。これを可能にするために、HelidonはJPA、Hibernate、Hikariと統合しています。しかし、もう一歩踏み込んでみましょう。
Helidonの最新リリースでは、Graal VMのネイティブイメージでのJPAをサポートするようになりました。これにより、Java Persistence APIのパワーをフルに使って、非常に軽量なマイクロサービスを実装できるようになりました。
現在サポートされている実装は、JPA実装としてのHibernateと、以下のデータベースのJDBCドライバのセットです。
- H2
- MySQL
- PostgreSQL
それでは、シンプルなRESTアプリケーション・インターフェースを備えたサンプルの永続化レイヤーを実装するプロセスを見てみましょう。Hello Worldアプリケーションを変更し、データベースに人の情報を保存するようにします。各人は、nick(ニックネーム)とname(本名)の2つの属性を持ちます。nickは、本名に変換されて “Hello World “のあいさつに付加されます。
Step 1: Generate MP Project
CLIで実行できます。公式Webサイトからダウンロードして、コンソールで以下のようにタイプしてください。
Helidon CLI
https://helidon.io/docs/v2/#/about/05_cli
> helidon init
いくつかの質問に答えると、QuicstartMP
プロジェクトができあがります。
もしくは、以下のMavenコマンドを実行してサンプルのMicroProfileプロジェクトを生成してください。
mvn -U archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=io.helidon.archetypes \
-DarchetypeArtifactId=helidon-quickstart-mp \
-DarchetypeVersion=2.1.0 \
-DgroupId=io.helidon.examples \
-DartifactId=helidon-jpa-native-image \
-Dpackage=io.helidon.examples.jpa.ni
このコマンドでは以下のファイルを含むhelidon-jpa-native-image
ディレクトリが作成されます。
helidon-jpa-native-image/.dockerignore
helidon-jpa-native-image/app.yaml
helidon-jpa-native-image/Dockerfile
helidon-jpa-native-image/Dockerfile.jlink
helidon-jpa-native-image/Dockerfile.native
helidon-jpa-native-image/pom.xml
helidon-jpa-native-image/README.md
helidon-jpa-native-image/src/main/java/io/helidon/examples/jpa/ni/GreetingProvider.java
helidon-jpa-native-image/src/main/java/io/helidon/examples/jpa/ni/GreetResource.java
helidon-jpa-native-image/src/main/java/io/helidon/examples/jpa/ni/package-info.java
helidon-jpa-native-image/src/main/resources/logging.properties
helidon-jpa-native-image/src/main/resources/META-INF/beans.xml
helidon-jpa-native-image/src/main/resources/META-INF/microprofile-config.properties
helidon-jpa-native-image/src/main/resources/META-INF/native-image/reflect-config.json
helidon-jpa-native-image/src/test/java/io/helidon/examples/jpa/ni/MainTest.java
この最初のアプリケーションが動作することを確認するため、単純にビルドしてみましょう。
cd helidon-jpa-native-image
mvn clean install
java -jar target/helidon-jpa-native-image.jar
RESTインターフェースを持つJavaのMicroProfileアプリケーションが実行されるはずです。ではWebクライアントを実行して動作するか確認しましょう。
curl -X GET http://localhost:8080/greet
{"message":"Hello World!"}
Step 2: Add JPA Dependencies
JPA実装にはJPAプロバイダとしてのHibernateと選択したデータベースのJDBCドライバが必要です。今回のサンプルではMySQLを選択しました。HibernateはHelidon統合モジュールの一時的な依存関係ですが、MySQLデータベースドライバを依存関係に追加しなければなりません。以下は必要なHelidon統合モジュールの依存関係です。
<dependency>
<groupId>io.helidon.integrations.cdi</groupId>
<artifactId>helidon-integrations-cdi-hibernate</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.integrations.cdi</groupId>
<artifactId>helidon-integrations-cdi-jta</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.integrations.cdi</groupId>
<artifactId>helidon-integrations-cdi-datasource-hikaricp</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.helidon.integrations.cdi</groupId>
<artifactId>helidon-integrations-cdi-jpa</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.helidon.integrations.db</groupId>
<artifactId>mysql</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
Step 3: Configure JPA Persistence Unit
Persistence Unit(永続性ユニット) の構成ファイルには、Hibernateがデータベースに接続しORマッパーを使ってデータを扱う上で必要な情報が含まれています。この構成ファイルを追加するには、プロジェクトディレクトリの配下に src/main/resources/META-INF/persistence.xml
を作成します。このファイルには以下の内容が含まれています。
<?xml version="1.0" encoding="UTF-8"?><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="hello" transaction-type="JTA">
<properties>
<property name="hibernate.dialect"
value="org.hibernate.dialect.MySQL5Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="none"/>
<property
name="hibernate.temp.use_jdbc_metadata_defaults"
value="false"/>
<property name="show_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
つづいて、以下の行を src/main/resources/META-INF/microprofile-config.properties
ファイルに追加します。
javax.sql.DataSource.test.dataSource.url=jdbc:mysql://localhost/helloworld?useSSL=false&allowPublicKeyRetrieval=true
javax.sql.DataSource.test.dataSource.user=user
javax.sql.DataSource.test.dataSource.password=p4ssw0rd
javax.sql.DataSource.test.dataSourceClassName=com.mysql.cj.jdbc.MysqlDataSource
Step 4: Run and Initialize the Database
必要な資格証明とデータベースを備えたMySQLを実行するなら、Dockerイメージを使用するのが一番簡単です。以下のコマンドを実行します。
docker run -- name mysql -e MYSQL_ROOT_PASSWORD=r00tp4ssw0rd \
-e MYSQL_USER=user -e MYSQL_PASSWORD=p4ssw0rd \
-e MYSQL_DATABASE=helloworld -p 3306:3306 mysql:8
このコマンドで、資格証明を構成し、helloworldというデータベース名を持つMySQL 8データベースが稼働します。データベース表はサーバー起動時に作成されていないため、手作業もしくはJPAのスキーマ生成機能を使って実施しなければなりません。JPAスキーマ生成機能を使うには、以下のプロパティを src/main/resources/META-INF/persistence.xml
ファイルのpropertiesセクションに追加します。
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
このプロパティをnoneに変更するまで、データベーススキーマはこのサンプル開始の都度リセットされます。
Step 5: Data Model
人のニックネームと本名をPerson
エンティティにマップします。以下の構成を src/main/resources/META-INF/persistence.xml
ファイルに追加する必要があります。
<class>io.helidon.examples.jpa.ni.Person</class>
Person
エンティティのコードです。
package io.helidon.examples.jpa.ni;import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;@Entity
public class Person {
@Id
@Column(columnDefinition = "VARCHAR(32)", nullable = false)
private String nick; private String name; public String getNick() {
return nick;
} public void setNick(String nick) {
this.nick = nick;
}
public String getName() {
return name;
} public void setName(String name) {
this.name = name;
}
}
Step 6: Resource Modification
JAX-RSリソースクラスのGreetResource
では、JPAおよびデータベースを扱うために変更が必要です。
Entity Manager
EntityManager
インスタンスを追加して、JPAコードの呼び出しを可能にする必要があります。これは、GreetResource
クラスのインスタンスのプライベート属性に過ぎません。
@PersistenceContext(unitName = "hello")
private EntityManager em;
JAX-RS request methods
新しいPersonレコードの作成を許可するためには、新たなJAX-RS POSTメソッドが必要です。
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Transactional
@POST
public Response createPerson(JsonObject jsonPerson) {
if (jsonPerson == null || !jsonPerson.containsKey("nick")
|| !jsonPerson.containsKey("name")) {
return Response
.status(Response.Status.fromStatusCode(422))
.build();
}
String nick = jsonPerson.getString("nick");
String name = jsonPerson.getString("name");
Person person = new Person();
person.setNick(nick);
person.setName(name);
JsonObjectBuilder entityBuilder = JSON.createObjectBuilder()
.add("nick", nick)
.add("name", name);
try {
em.persist(person);
return Response.status(Response.Status.OK)
.entity(entityBuilder.build())
.build();
} catch (PersistenceException pe) {
pe.printStackTrace();
JsonObject entity = entityBuilder
.add("error", pe.getMessage())
.build();
return Response
.status(Response.Status.CONFLICT)
.entity(entity)
.build();
}
}
最終的に、getMessage
メソッドを変更して、データベースのリクエストからnick
にマップされた名前を取得できるようにする必要があります。
@Path("/{nick}")
@GET
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public Response getMessage(@PathParam("nick") String nick) {
Person entity = em.find(Person.class, nick);
JsonObjectBuilder entityBuilder = JSON.createObjectBuilder()
.add("nick", nick);
if (entity == null) {
JsonObject responseEntity = entityBuilder
.add("error", String.format(
"Nick %s was not found", nick))
.build();
return Response
.status(Response.Status.CONFLICT)
.entity(responseEntity)
.build();
}
JsonObject responseEntity = createResponse(entity.getName());
return Response
.status(Response.Status.OK)
.entity(responseEntity)
.build();
}
Step 7: Native Image
JPAのバイトコードをコンパイル時に生成すると共に、 hibernate.bytecode.provider
を無効にする必要があります。このために、 src/main/resources/hibernate.properties
という新たなファイルで以下の設定をする必要があります。
hibernate.bytecode.provider=none
追加の依存関係をpom.xmlに追加します。
<dependency>
<groupId>io.helidon.integrations.cdi</groupId>
<artifactId>helidon-integrations-cdi-jta-weld</artifactId>
</dependency>
Step 8: Tests Modification
最後の変更点はJUnitテストです。この時点では、プロジェクトに対して全ての変更が施されるとテストに失敗するため、MainTest
クラスを変更します。
package io.helidon.examples.jpa.ni;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.spi.CDI;
import javax.json.Json;
import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;import io.helidon.microprofile.server.Server;import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;class MainTest { private static Server server;
private static String serverUrl; @BeforeAll
public static void startTheServer() throws Exception {
server = Server.create().start();
serverUrl = "http://localhost:" + server.port();
} @Test
@Order(1)
void testAddPerson() {
Client client = ClientBuilder.newClient();
JsonObject jsonObject = Json.createObjectBuilder()
.add("nick", "joe")
.add("name", "Joe Brown")
.build();
Response r = client
.target(serverUrl)
.path("greet")
.request()
.post(Entity.entity(jsonObject.toString(),
MediaType.APPLICATION_JSON));
Assertions.assertEquals(200, r.getStatus(),
"POST person status code");
jsonObject = Json.createObjectBuilder()
.add("nick", "jose")
.add("name", "Jose Carreras")
.build();
r = client
.target(serverUrl)
.path("greet")
.request()
.post(Entity.entity(jsonObject.toString(),
MediaType.APPLICATION_JSON));
Assertions.assertEquals(200, r.getStatus(),
"POST person status code");
} @Test
@Order(1)
void testHelloWorld() {
Client client = ClientBuilder.newClient();
JsonObject jsonObject = client
.target(serverUrl)
.path("greet")
.request()
.get(JsonObject.class);
Assertions.assertEquals("Hello World!",
jsonObject.getString("message"),
"default message");
jsonObject = client
.target(serverUrl)
.path("greet/joe")
.request()
.get(JsonObject.class);
Assertions.assertEquals("Hello Joe Brown!",
jsonObject.getString("message"),
"hello Joe message");
Response r = client
.target(serverUrl)
.path("greet/greeting")
.request()
.put(Entity.entity("{\"greeting\" : \"Hola\"}",
MediaType.APPLICATION_JSON));
Assertions.assertEquals(204, r.getStatus(),
"PUT status code");
jsonObject = client
.target(serverUrl)
.path("greet/Jose")
.request()
.get(JsonObject.class);
Assertions.assertEquals("Hola Jose Carreras!",
jsonObject.getString("message"),
"hola Jose message");
r = client
.target(serverUrl)
.path("metrics")
.request()
.get();
Assertions.assertEquals(200, r.getStatus(),
"GET metrics status code");
r = client
.target(serverUrl)
.path("health")
.request()
.get();
Assertions.assertEquals(200, r.getStatus(),
"GET health status code");
} @AfterAll
static void destroyClass() {
CDI<Object> current = CDI.current();
((SeContainer) current).close();
}
}
テスト間のデータの依存関係があるため、これはもはや真のjUnitではないことに注意してください。あくまでも使用例としてお考えください。
Step 9: Building and Testing
まず、ビルド環境にネイティブイメージをサポートするgraalvm-ce-java11– 21.0.0.2がインストール済みであることを確認しましょう。その上で、この変更したプロジェクトをネイティブイメージにするために以下を実行します。
mvn clean install -Pnative-image
少々時間がかかります。ビルドが成功すれば、データベースサーバーが稼働していることを確認して、以下のコマンドを実行します。
target/helidon-jpa-native-image
では、単純なあいさつの動作確認から。
curl -X GET http://localhost:8080/greet
レスポンスは以下のようになるはずです。
{"message":"Hello World!"}
ではデータベースに新たな人の情報を追加します。
curl -X POST -H "Content-Type: application/json" \
-d '{"nick":"bob","name":"Bobby Fischer"}' \
http://localhost:8080/greet
この操作のレスポンスは以下のようなものになるはずです。
{"nick":"bob","name":"Bobby Fischer"}
では、追加した人に対してあいさつしてみましょう。
curl -X GET http://localhost:8080/greet/bob
レスポンスは以下のようになるはずです。
{"message":"Hello Bobby Fischer!"}
Conclusion
このサンプルでは、Helidon MPアプリケーションの実装とネイティブイメージでのコンパイル方法をご紹介しました。ちょっとの手順で、データベースに接続する、完全に機能するMicroProfileベースのマイクロサービスを、1つの実行ファイルとして作成することができました。数種類のデータベースのJDBCドライバが既にネイティブイメージをサポートしており、今後追加される予定です。
(訳注)Oracle DatabaseはOracle Database 21cのJDBCドライバで利用可能のようです。詳細は以下を参照ください。
What’s in Oracle Database 21c for Java Developers?
https://www.oracle.com/a/otn/docs/what-is-in-db21c-for-java-developers.pdf
ここで紹介したプロジェクトは以下のGitHubリポジトリにあります。
Data Persistence with Helidon and Native Image
https://github.com/Tomas-Kraus/helidon-jpa-native-image