The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

PanacheでJakarta RESTリソースを生成する

多くのWebアプリケーションは、REST APIを使った単調なCRUDアプリケーションで、書くのが面倒です。このタスクを合理化するために、REST Data with Panache エクステンションは、エンティティーやリポジトリの基本的なCRUDエンドポイントを生成することができます。

このエクステンションはまだ実験的なものであり、限られた機能セットしか提供していませんが、早期にフィードバックが得られることを期待しています。現在のところ、このエクステンションは Hibernate ORM と MongoDB with Panache をサポートしており、 application/jsonapplication/hal+json のコンテンツで動作する CRUD リソースを生成することができます。

REST Data with Panacheのセットアップ

Quarkusは、REST Data with Panacheをセットアップするために、以下のエクステンションを提供しています。次の互換性表を参照して、使用している技術に応じて適切なものを使用してください:

Table 1. 互換性表
エクステンション Hibernate ORM RESTEasy

quarkus-hibernate-orm-rest-data-panache

ORM

Classic and Reactive

quarkus-hibernate-reactive-rest-data-panache

Reactive

Reactive

quarkus-mongodb-rest-data-panache

ORM

Classic and Reactive

Hibernate ORM

  • 必要な依存関係を pom.xml に追加します

    • Hibernate ORM REST Data with Panache エクステンション ( quarkus-hibernate-orm-rest-data-panache )

    • JDBC ドライバーエクステンション ( quarkus-jdbc-postgresql , quarkus-jdbc-h2 , quarkus-jdbc-mariadb , …​)

    • RESTEasy JSON シリアライゼーションエクステンションのどれか ( RESTEasy Classic と RESTEasy Reactive の両方をサポートしたエクステンション)

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm-rest-data-panache</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>

<!-- Use this if you are using RESTEasy Reactive -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>

<!-- Use this if you are going to use RESTEasy Classic -->
<!--
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
-->
build.gradle
implementation("io.quarkus:quarkus-hibernate-orm-rest-data-panache")
implementation("io.quarkus:quarkus-jdbc-postgresql")

// Use this if you are using RESTEasy Reactive
implementation("io.quarkus:quarkus-resteasy-reactive-jackson")

// Use this if you are going to use RESTEasy Classic
// implementation("io.quarkus:quarkus-resteasy-jackson")
  • Hibernate ORM with Panache ガイドで説明されているように、Panache エンティティおよび/またはリポジトリを実装します。

  • リソース生成 のセクションで説明したように、生成のためのインターフェイスを定義します。

Hibernate ORM REST Data with Panacheの動作を確認するには、 https://github.com/quarkusio/quarkus-quickstarts/tree/main/hibernate-orm-rest-data-panache-quickstart [hibernate-orm-rest-data-panache-quickstart] クイックスタートで確認することができます。

Hibernate Reactive

  • 必要な依存関係を pom.xml に追加します

    • Hibernate ORM REST Data with Panache エクステンション ( quarkus-hibernate-orm-rest-data-panache )

    • Vert.x リアクティブデータベースドライバエクステンション ( quarkus-reactive-pg-client, quarkus-reactive-mysql-client, …​)

    • RESTEasy JSON シリアライゼーションエクステンションのどれか ( quarkus-resteasy-jackson または quarkus-resteasy-jsonb )

<dependencies>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-reactive-rest-data-panache</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-reactive-pg-client</artifactId>
    </dependency>
   <!-- Use this if you are using RESTEasy Reactive Jackson for serialization -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
    </dependency>
</dependencies>
  • Hibernate Reactive with Panache ガイドの説明に従って、Panacheエンティティやリポジトリを実装します。

  • リソース生成 の項で説明したように、生成のためのインターフェイスを定義する。

MongoDB

  • 必要な依存関係を pom.xml に追加します

    • MongoDB REST Data with Panache エクステンション ( quarkus-mongodb-rest-data-panache )

    • RESTEasy JSON シリアライゼーションエクステンションのどれか ( quarkus-resteasy-jackson または quarkus-resteasy-jsonb )

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-mongodb-rest-data-panache</artifactId>
</dependency>

<!-- Use this if you are using RESTEasy Reactive -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>

<!-- Use this if you are going to use RESTEasy Classic -->
<!--
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
-->
build.gradle
implementation("io.quarkus:quarkus-mongodb-rest-data-panache")

// Use this if you are using RESTEasy Reactive
implementation("io.quarkus:quarkus-resteasy-reactive-jackson")

// Use this if you are going to use RESTEasy Classic
// implementation("io.quarkus:quarkus-resteasy-jackson")
  • Implement the Panache entities and/or repositories as explained in the MongoDB with Panache guide.

  • リソース生成 の項で説明したように、生成のためのインターフェイスを定義する。

リソースの生成

REST Data with Panacheは、アプリケーションに存在するインターフェースに基づいて、Jakarta RESTリソースを生成します。生成したい各エンティティとリポジトリに対して、リソース・インターフェースを提供します。 これらのインターフェイスは実装せず、カスタムメソッドを提供しないようにしてください。 ただし、カスタマイズするために、拡張インターフェースからメソッドをオーバーライドすることは可能です(末尾のセクションを参照)。

PanacheEntityResource

If your application has an entity (e.g. Person) that extends either PanacheEntity or PanacheEntityBase class, you could instruct REST Data with Panache to generate its Jakarta REST resource with the following interface:

public interface PeopleResource extends PanacheEntityResource<Person, Long> {
}

PanacheRepositoryResource

If your application has a simple entity (e.g. Person) and a repository (e.g. PersonRepository) that implements either PanacheRepository or PanacheRepositoryBase interface, you could instruct REST Data with Panache to generate its Jakarta REST resource with the following interface:

public interface PeopleResource extends PanacheRepositoryResource<PersonRepository, Person, Long> {
}

PanacheMongoEntityResource

アプリケーションに PanacheMongoEntity または PanacheMongoEntityBase クラスを拡張したエンティティ (例: Person ) がある場合、REST Data with Panache に指示して、次のインターフェイスで Jakarta REST リソースを生成させることができます:

public interface PeopleResource extends PanacheMongoEntityResource<Person, Long> {
}

PanacheMongoRepositoryResource

アプリケーションにシンプルなエンティティ(例: Person )と、 PanacheMongoRepository または PanacheMongoRepositoryBase インターフェースを実装するリポジトリ(例: PersonRepository )がある場合、REST Data with Panache に指示して、次のインターフェイスで Jakarta REST リソースを生成させることができます:

public interface PeopleResource extends PanacheMongoRepositoryResource<PersonRepository, Person, Long> {
}

生成されたリソース

生成されるリソースは、エンティティーとリポジトリの両方で機能的に同等となります。唯一の違いは、利用時の特定のデータアクセスパターンとデータストレージです。

If you have defined one of the PeopleResource interfaces mentioned above, this extension will generate its implementation using a particular data access strategy. The implemented class then will be used by a generated Jakarta REST resource, which will look like this:

public class PeopleResourceJaxRs { // The actual class name is going to be unique
    @Inject
    PeopleResource resource;

    @GET
    @Path("{id}")
    @Produces("application/json")
    public Person get(@PathParam("id") Long id){
        Person person = resource.get(id);
        if (person == null) {
            throw new WebApplicationException(404);
        }
        return person;
    }

    @GET
    @Produces("application/json")
    public Response list(@QueryParam("sort") List<String> sortQuery,
            @QueryParam("page") @DefaultValue("0") int pageIndex,
            @QueryParam("size") @DefaultValue("20") int pageSize) {
        Page page = Page.of(pageIndex, pageSize);
        Sort sort = getSortFromQuery(sortQuery);
        List<Person> people = resource.list(page, sort);
        // ... build a response with page links and return a 200 response with a list
    }

    @GET
    @Path("/count")
    public long count() {
        return resource.count();
    }

    @Transactional
    @POST
    @Consumes("application/json")
    @Produces("application/json")
    public Response add(Person personToSave) {
        Person person = resource.add(person);
        // ... build a new location URL and return 201 response with an entity
    }

    @Transactional
    @PUT
    @Path("{id}")
    @Consumes("application/json")
    @Produces("application/json")
    public Response update(@PathParam("id") Long id, Person personToSave) {
        if (resource.get(id) == null) {
            Person person = resource.update(id, personToSave);
            return Response.status(204).build();
        }
        Person person = resource.update(id, personToSave);
        // ... build a new location URL and return 201 response with an entity
    }

    @Transactional
    @DELETE
    @Path("{id}")
    public void delete(@PathParam("id") Long id) {
        if (!resource.delete(id)) {
            throw new WebApplicationException(404);
        }
    }
}

リソースのカスタマイズ

REST Data with Panache provides a @ResourceProperties and @MethodProperties annotations that can be used to customize certain features of the resource.

It can be used in your resource interface:

@ResourceProperties(hal = true, path = "my-people")
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
    @MethodProperties(path = "all")
    List<Person> list(Page page, Sort sort);

    @MethodProperties(exposed = false)
    boolean delete(Long id);
}

利用可能なオプション

@ResourceProperties

  • exposed - リソースが公開される可能性があるかどうか。各メソッドに対してオーバーライド可能なグローバルリソースプロパティー。デフォルトは true です。

  • path - リソースのベースパス。デフォルトのパスは、 resource または controller のサフィックスを含まないハイフン付きの小文字のリソース名です。

  • rolesAllowed - List of the security roles permitted to access the resources. It needs a Quarkus Security extension to be present, otherwise it will be ignored. Default is empty.

  • paged - コレクションのレスポンスをページングするかどうか。最初、最後、前、次のページの URI が存在する場合は、レスポンスヘッダに含まれます。リクエストページのインデックスとサイズは、 pagesize のクエリパラメーターから取得され、それぞれのデフォルトは 020 です。デフォルトは true です。

  • hal - 標準の application/json レスポンスに加えて、 Accept ヘッダでリクエストされた場合に application/hal+json レスポンスを返す追加のメソッドを生成します。デフォルトは false です。

  • halCollectionName - HAL コレクションレスポンスを生成する際に使用されるべき名前です。デフォルトの名前は resource または controller のサフィックスなしのハイフン付き小文字のリソース名です。

@MethodProperties

  • exposed - false に設定されている場合、特定の HTTP Verb を公開しません。デフォルトは true です。

  • path - 操作パス (これはリソースベースのパスに追加されます)。デフォルトは空の文字列です。

  • rolesAllowed - List of the security roles permitted to access this operation. It needs a Quarkus Security extension to be present, otherwise it will be ignored. Default is empty.

生成されたリソースへの追加メソッドの追加

REST Data with Panache エクステンションによって生成されたリソースに対して、リソースインターフェイスにこれらのメソッドを追加することで、追加メソッドを追加することができます。例:

@ResourceProperties
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
    @GET
    @Path("/name/{name}")
    @Produces("application/json")
    default List<Person> findByName(@PathParam("name") String name) {
        return Person.find("name = :name", Collections.singletonMap("name", name)).list();
    }
}

そして、このメソッドは、生成されたメソッドとともに、 http://localhost:8080/people/name/Johan で公開されます。

エンドポイントのセキュア化

REST Data with Panache will use the Security annotations within the package jakarta.annotation.security that are defined on your resource interfaces:

import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.RolesAllowed;

@DenyAll
@ResourceProperties
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
    @RolesAllowed("superuser")
    boolean delete(Long id);
}

さらに、リソースの使用が許可されているロールの指定にのみ関心がある場合、 @ResourceProperties および @MethodProperties のアノテーションには、リソースまたは操作へのアクセスが許可されているセキュリティロールをリストアップするフィールド rolesAllowed があります。

エンティティをリストアップするクエリパラメータ

REST Data with Panacheは、エンティティのリストを取得するために以下のクエリパラメータをサポートしています:

  • page - リスト操作で返されるべきページ番号。これはページ化されたリソースにのみ適用され、0 から始まる番号です。 デフォルトは 0 です。

  • size - リスト操作で返されるべきページサイズ。これはページ化されたリソースにのみ適用され、1から始まる数値です。 デフォルトは20です。

  • sort - リスト操作の結果をソートするために使われるべきフィールドのカンマ区切りのリスト。フィールドの前に - を付けない限り、フィールドは昇順でソートされます。例えば ?sort=name,-age は、名前の昇順、年齢の降順で結果をソートします。

  • namedQuery - `@NamedQuery`アノテーションを用いてエンティティレベルで設定されるべき名前付きクエリです。

たとえば、最初のページで2つの People エンティティを取得したい場合、 http://localhost:8080/people?page=0&size=2 を呼び出します。応答は次のようになる筈です:

[
  {
    "id": 1,
    "name": "John Johnson",
    "birth": "1988-01-10"
  },
  {
    "id": 2,
    "name": "Peter Peterson",
    "birth": "1986-11-20"
  }
]

さらに、フィールド名と値を含むクエリパラメータを追加することで、エンティティフィールドでフィルタリングすることも可能です。 http://localhost:8080/people?name=Peter Peterson を呼び出すと、次が返却されるでしょう。

[
  {
    "id": 2,
    "name": "Peter Peterson",
    "birth": "1986-11-20"
  }
]
Filtering by fields is only supported for String, Boolean, Character, Double, Float, Integer, Long, Short, Byte and the primitive types.

@NamedQueryを使った複雑なフィルタリングによるエンティティのリスト化

エンティティのリストアップ時にフィルタリングする名前付きクエリを指定することができます。例えば、エンティティに次のような名前付きクエリを持つことです:

@Entity
@NamedQuery(name = "Person.containsInName", query = "from Person where name like CONCAT('%', CONCAT(:name, '%'))")
public class Person extends PanacheEntity {
  String name;
}

この例では、 name フィールドにテキストを含むすべての人物をリストアップする名前付きクエリを追加しています。

次に、使用したい名前付きクエリの名前で生成されたリソースを使用してエンティティを一覧表示するときに、クエリ パラメータ namedQuery を設定できます。 http://localhost:8080/people?namedQuery=Person.containsInName&name=ter の呼出は、名前に "ter"という文字列が含まれる人物をすべて返します。

For more information about how named queries work, go to the Hibernate ORM guide or to the Hibernate Reactive guide.

リソースメソッド Before/Afterリスナー

REST Data with Panacheは、以下のリソースメソッドフックの購読をサポートします:

  • Before/After add resource

  • Before/After update resource

  • Before/After delete resource

リソースメソッドリスナーを登録するには、インターフェース RestDataResourceMethodListener を実装したBeanを提供する必要があります。例:

@ApplicationScoped
public class PeopleRestDataResourceMethodListener implements RestDataResourceMethodListener<Person> {
    @Override
    public void onBeforeAdd(Person person) {
        System.out.println("Before Save Person: " + person.name);
    }
}

レスポンスボディの例

前述の通り、REST Data with Panache は application/jsonapplication/hal+json のレスポンス コンテンツ タイプをサポートしています。 Person ここでは、データベース内に get レコードが 5 つあると仮定して、 と list 操作を行った場合のレスポンスボディがどのようになるか、いくつかの例を示します。

GET /people/1

Accept: application/json

{
  "id": 1,
  "name": "John Johnson",
  "birth": "1988-01-10"
}

Accept: application/hal+json

{
  "id": 1,
  "name": "John Johnson",
  "birth": "1988-01-10",
  "_links": {
    "self": {
      "href": "http://example.com/people/1"
    },
    "remove": {
      "href": "http://example.com/people/1"
    },
    "update": {
      "href": "http://example.com/people/1"
    },
    "add": {
      "href": "http://example.com/people"
    },
    "list": {
      "href": "http://example.com/people"
    }
  }
}

GET /people?page=0&size=2

Accept: application/json

[
  {
    "id": 1,
    "name": "John Johnson",
    "birth": "1988-01-10"
  },
  {
    "id": 2,
    "name": "Peter Peterson",
    "birth": "1986-11-20"
  }
]

Accept: application/hal+json

{
  "_embedded": [
    {
      "id": 1,
      "name": "John Johnson",
      "birth": "1988-01-10",
      "_links": {
        "self": {
          "href": "http://example.com/people/1"
        },
        "remove": {
          "href": "http://example.com/people/1"
        },
        "update": {
          "href": "http://example.com/people/1"
        },
        "add": {
          "href": "http://example.com/people"
        },
        "list": {
          "href": "http://example.com/people"
        }
      }
    },
    {
      "id": 2,
      "name": "Peter Peterson",
      "birth": "1986-11-20",
      "_links": {
        "self": {
          "href": "http://example.com/people/2"
        },
        "remove": {
          "href": "http://example.com/people/2"
        },
        "update": {
          "href": "http://example.com/people/2"
        },
        "add": {
          "href": "http://example.com/people"
        },
        "list": {
          "href": "http://example.com/people"
        }
      }
    }
  ],
  "_links": {
    "add": {
      "href": "http://example.com/people"
    },
    "list": {
      "href": "http://example.com/people"
    },
    "first": {
      "href": "http://example.com/people?page=0&size=2"
    },
    "last": {
      "href": "http://example.com/people?page=2&size=2"
    },
    "next": {
      "href": "http://example.com/people?page=1&size=2"
    }
  }
}

両方のレスポンスには、これらのヘッダも含まれています:

前のページが存在しないため、 previous リンクヘッダ(および HAL リンク)が含まれません。