Generating JAX-RS resources with Panache

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

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

REST Data with Panacheのセットアップ

Quarkusは、PanacheでREST Dataを設定するために、以下のエクステンションを提供しています。次の互換性テーブルを参照して、使用している技術に応じて適切なものを使用してください:

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 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>

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")

リソースの生成

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

PanacheEntityResource

アプリケーションに PanacheEntity または PanacheEntityBase クラスを拡張したエンティティー ( Person など) がある場合、REST Data with Panache に、次のインターフェイスを使用して JAX-RS リソースを生成するように指示することができます。

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

PanacheRepositoryResource

アプリケーションが単純なエンティティー(例: Person )と、 PanacheRepository または PanacheRepositoryBase インターフェイスを実装したリポジトリ(例: PersonRepository )を持っている場合、REST Data with Panache に、以下のインターフェイスを使用して JAX-RS リソースを生成するように指示することができます。

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

PanacheMongoEntityResource

アプリケーションに PanacheMongoEntity または PanacheMongoEntityBase クラスを拡張したエンティティー ( Person など) がある場合、REST Data with Panache に、次のインターフェイスを使用して JAX-RS リソースを生成するように指示することができます。

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

PanacheMongoRepositoryResource

アプリケーションが単純なエンティティー(例: Person )と、 PanacheMongoRepository または PanacheMongoRepositoryBase インターフェイスを実装したリポジトリ(例: PersonRepository )を持っている場合、REST Data with Panache に、以下のインターフェイスを使用して JAX-RS リソースを生成するように指示することができます。

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

生成されたリソース

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

上記の PeopleResource インターフェイスのいずれかを定義している場合、このエクステンションは特定のデータアクセス戦略を使用してその実装を生成します。実装されたクラスは、生成された JAX-RS リソースによって使用され、以下のようになります。

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 は、リソースの特定の機能をカスタマイズするために使用できる @ResourceProperties と @MethodProperties のアノテーションを提供します。リソースのインターフェイスで使用することができます。

@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 - リソースへのアクセスが許可されているセキュリティロールのリストです。Quarkusのセキュリティエクステンションが存在する必要があり、存在しない場合は無視されます。デフォルトは空です。

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

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

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

@MethodProperties

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

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

  • rolesAllowed - この操作へのアクセスが許可されているセキュリティロールのリスト。Quarkusのセキュリティエクステンションが存在する必要があり、存在しない場合は無視されます。デフォルトは空です。

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

REST Data with Panacheは、リソースインターフェースに定義された、パッケージ javax.annotation.security のSecurityアノテーションを使用します:

import javax.annotation.security.DenyAll;
import javax.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 は、名前の昇順、年齢の降順で結果をソートします。

レスポンスボディの例

前述の通り、REST Data with Panache は application/json と application/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 リンク)が含まれません。