Data Persistence with Helidon and Native Image

原文はこちら。
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

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中