Quarkus CLI による Panache CRUD アプリケーションの作り方

f:id:Naotsugu:20210901204009p:plain


はじめに

今回は、Quarkus CLI を使ったアプリケーション作成の流れについて説明します。

Panache を使った簡単な CRUD アプリケーションを例に取り上げましょう。

Quarkus CLI の詳細については以下を参照してください。

blog1.mammb.com


プロジェクトの作成

Quarkus CLI にてプロジェクトを作成します。

$ quarkus create app --gradle --java -x=quarkus-resteasy-jackson,quarkus-hibernate-orm-panache,quarkus-jdbc-h2 com.mammb:quarkus-panache:0.1.0

以下の Extension を追加しました。

  • io.quarkus:quarkus-resteasy-jackson
  • hibernate-orm-panache
  • quarkus-jdbc-h2

プロジェクトは以下のように作成されます。

-----------
selected extensions:
- io.quarkus:quarkus-hibernate-orm-panache
- io.quarkus:quarkus-jdbc-h2
- io.quarkus:quarkus-resteasy-jackson

applying codestarts...
📚  java
🔨  gradle
📦  quarkus
📝  config-properties
🔧  dockerfiles
🔧  gradle-wrapper
🚀  resteasy-codestart

-----------
[SUCCESS] ✅  quarkus project has been successfully generated in:
--> /<path>/quarkus-panache
-----------
Navigate into this directory and get started: quarkus dev


開発モードの起動

プロジェクトが作成されたら開発モードで起動します。

$ quarkus dev

開発モードで d を入力すれば Dev UI が起動します。

f:id:Naotsugu:20210904230938p:plain

導入した Extension が表示されます。

Configuration にある Config Editor からデータベースの自動生成の項目の設定を入れておきましょう。

quarkus.hibernate-orm.database.generation を検索し、drop-and-create に設定します。

f:id:Naotsugu:20210904231222p:plain

✅ ボタンで反映すれば、src/main/resources/application.properties に編集内容が反映されます。これにより、データベースのテーブルがアプリケーション起動時にドロップされて再作成されるようになります。


開発モードではライブコーディングが有効になっているため、後はソースを書いていくだけで、即座に変更内容を確認することができます。


Entity を作成する

Panache による Entity を作成します。今回は Repository は利用せず、PanacheEntity を継承して Entity を作成します。

単純な Person をsrc/main/java/com/mammb/Person.java に作成します。

package com.mammb;

import javax.persistence.Column;
import javax.persistence.Entity;
import java.time.LocalDate;
import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Person extends PanacheEntity {
    @Column(length = 120, unique = true)
    public String name;
    public LocalDate birth;
}

Panache では、属性は public で定義して getter/setter は書きません。コンパイル時に自動生成されます。 getter/setter を明示的に定義すれば そちらが使用されます。

PanacheEntity には Entity 操作のメソッドが定義されているため、Entity に関連する準備はこれで完了です。


JAX-RS リソースを作成する

JAX-RS リソース をsrc/main/java/com/mammb/PersonResource.java に作成します。

package com.mammb;

import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import io.quarkus.panache.common.Sort;

@Path("/persons")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PersonResource {
    
    @GET
    public List<Person> get() {
        return Person.listAll(Sort.by("name"));
    }

    @POST
    @Transactional
    public Response create(Person person) {
        if (person.id != null) {
            throw new WebApplicationException("Id was invalidly set on request.", 422);
        }
        person.persist();
        return Response.ok(person).status(201).build();
    }
}

一覧取得と新規登録用のメソッドを追加しました。

Person.listAll() でデータベースから select、person.persist() でデータベースへの insert 操作となります。

この時点で、w を押せばブラウザが起動してアプリケーションを確認できます。


Continuous Testing

Quarkus 2.0 で導入された Continuous Testing でテストはコードを変更する毎に自動で実行されます。コンソール(または Dev UI)で r を押すことで実行モードを切り替えることができます。

PersonResource のテストをsrc/test/java/com/mammb/PersonResourceTest.java に作成しましょう。

package com.mammb;

import javax.ws.rs.core.MediaType;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
public class PersonResourceTest {

    @Test
    public void testPersonEndpoint() {

        given()
                .body("{'name':'Quarkus','birth':'2000-01-01'}".replace('\'', '"'))
                .header("Content-Type", MediaType.APPLICATION_JSON)
                .when().post("/persons")
                .then()
                .statusCode(201)
                .body("name", is("Quarkus"));

        given()
                .when().get("/persons")
                .then()
                .statusCode(200)
                .body(containsString("Quarkus"));
    }
}

コードを変更すると、その場で関連するテストが自動実行され、もし失敗するテストがあれば、以下のようにテストの失敗がレポートされます。

f:id:Naotsugu:20210904232258p:plain

コードを修正すれば再びテストがされます。

Continuous Testing では、このようにコード変更に応じて継続的にテストを実行することができます。


CRUD メソッドの追加

PersonResource に、その他の CRUD メソッドを追加しましょう。

public class PersonResource {

    // ...

    @GET
    @Path("{id}")
    public Person getSingle(@PathParam Long id) {
        Person entity = Person.findById(id);
        if (entity == null) {
            throw new WebApplicationException("Fruit with id of " + id + " does not exist.", 404);
        }
        return entity;
    }

    @PUT
    @Path("{id}")
    @Transactional
    public Person update(@PathParam Long id, Person person) {
        if (person.name == null) {
            throw new WebApplicationException("Fruit Name was not set on request.", 422);
        }

        Person entity = Person.findById(id);
        if (entity == null) {
            throw new WebApplicationException("Fruit with id of " + id + " does not exist.", 404);
        }

        entity.name = person.name;
        entity.birth = person.birth;

        return entity;
    }

    @DELETE
    @Path("{id}")
    @Transactional
    public Response delete(@PathParam Long id) {
        Person entity = Person.findById(id);
        if (entity == null) {
            throw new WebApplicationException("Fruit with id of " + id + " does not exist.", 404);
        }
        entity.delete();
        return Response.status(204).build();
    }
}


合わせてテストも追加しておきましょう。

@QuarkusTest
public class PersonResourceTest {

    @Test
    public void testPersonEndpoint() {

        Integer id = given()
                .header("Content-Type", MediaType.APPLICATION_JSON)
                .body("{'name':'Quarkus','birth':'2000-01-01'}".replace('\'', '"'))
                .when().post("/persons")
                .then()
                .statusCode(201)
                .body("name", is("Quarkus"))
                .extract().path("id");

        given()
                .when().get("/persons/" + id)
                .then()
                .statusCode(200)
                .body("name", is("Quarkus"),
                      "birth", is("2000-01-01"));

        given()
                .header("Content-Type", MediaType.APPLICATION_JSON)
                .body("{'name':'Quarkus2','birth':'2000-01-01'}".replace('\'', '"'))
                .when().put("/persons/" + id)
                .then()
                .statusCode(200)
                .body("name", is("Quarkus2"));

        given()
                .when().delete("/persons/" + id)
                .then()
                .statusCode(204);

        given()
                .when().get("/persons")
                .then()
                .statusCode(200)
                .body(is("[]"));
    }
}

アプリケーションは一先ずこれで完了です。


OpenAPI と Swagger UI

作成したエンドポイントの OpenAPI Specification を作成します。

開発コンソールを q で一旦抜けて、OpenAPI の Extension を追加します。

$ quarkus ext add quarkus-smallrye-openapi

Extension が追加できたら quarkus dev で再度開発モードで起動します。

d で Dev UI を見ると、SmallRye OpenAPI として Extension が追加されたことが確認できます。

f:id:Naotsugu:20210904232941p:plain

openapi のリンクを押せば、OpenAPI Specification ファイルがダウンロードできます。

---
openapi: 3.0.3
info:
  title: quarkus-panache API
  version: 0.1.0
paths:
  /hello:
    get:
      responses:
        "200":
          description: OK
          content:
            text/plain:
              schema:
                type: string
  /persons:
    get:
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Person'
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Person'
      responses:
        "200":
          description: OK
  /persons/{id}:
    get:
      parameters:
      - name: id
        in: path
        required: true
        schema:
          format: int64
          type: integer
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Person'
    put:
      parameters:
      - name: id
        in: path
        required: true
        schema:
          format: int64
          type: integer
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Person'
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Person'
    delete:
      parameters:
      - name: id
        in: path
        required: true
        schema:
          format: int64
          type: integer
      responses:
        "200":
          description: OK
components:
  schemas:
    Person:
      type: object
      properties:
        id:
          format: int64
          type: integer
        birth:
          format: date
          type: string
        name:
          type: string

APIタイトル やバージョンなどの API情報は、MicroProfile OpenAPI アノテーションで Application クラスに定義することができる他、application.properties に記載することもできます(ここでは省略します)。


Swagger UI は同様に SmallRye OpenAPI のリンクから開くことができます。

f:id:Naotsugu:20210904233700p:plain

API 仕様の確認や、API の実行を簡単に行うことができます。


まとめ

Quarkus による簡単な CRUD 操作を行うアプリケーション作成の流れについて説明しました。

Quarkus CLI コマンドと Dev Services だけを操作していれば良いため、開発作業の立ち上げがとても簡素になりますので、試してみると良いと思います。