Generating Jakarta REST resources with Panache

很多 Web 应用程序都是单调的 CRUD 应用程序,包含非常乏味的 REST API。为了精简此任务,具有 Panache 的 REST 数据扩展可以为你的实体和存储库生成基本的 CRUD 终结点。

A lot of web applications are monotonous CRUD applications with REST APIs that are tedious to write. To streamline this task, REST Data with Panache extension can generate the basic CRUD endpoints for your entities and repositories.

虽然此扩展仍然处于试验阶段且提供的功能集有限,但我们希望尽早收到反馈。目前,此扩展支持带 Panache 的 Hibernate ORM 和 MongoDB,且可以生成适用于 application/jsonapplication/hal+json 内容的 CRUD 资源。

While this extension is still experimental and provides a limited feature set, we hope to get an early feedback for it. Currently, this extension supports Hibernate ORM and MongoDB with Panache and can generate CRUD resources that work with application/json and application/hal+json content.

Setting up REST Data with Panache

Quarkus 提供了以下扩展来设置 REST Data with Panache。请查看下一兼容性表格,以根据你正在使用的技术来使用正确的扩展:

Quarkus provides the following extensions to set up REST Data with Panache. Please, check out the next compatibility table to use the right one according to the technology you are using:

Table 1. Compatibility Table
Extension Hibernate RESTEasy

hr-hibernate-orm

ORM

Classic and Reactive

hr-hibernate-reactive

Reactive

Reactive

hr-mongodb

ORM

Classic and Reactive

Hibernate ORM

  • Add the required dependencies to your build file

    • Hibernate ORM REST Data with Panache extension (quarkus-hibernate-orm-rest-data-panache)

    • A JDBC driver extension (quarkus-jdbc-postgresql, quarkus-jdbc-h2, quarkus-jdbc-mariadb, …​)

    • One of the RESTEasy JSON serialization extensions (the extension supports both Quarkus REST (formerly RESTEasy Reactive) and RESTEasy Classic)

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 Quarkus REST -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-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 Quarkus REST
implementation("io.quarkus:quarkus-rest-jackson")

// Use this if you are going to use RESTEasy Classic
// implementation("io.quarkus:quarkus-resteasy-jackson")

若要查看具有 Panache 的 Hibernate ORM REST Data 的实际操作情况,请查看 hibernate-orm-rest-data-panache-quickstart 快速启动。

To see the Hibernate ORM REST Data with Panache in action, you can check out the hibernate-orm-rest-data-panache-quickstart quickstart.

Hibernate Reactive

  • Add the required dependencies to your pom.xml

    • Hibernate Reactive REST Data with Panache extension (quarkus-hibernate-reactive-rest-data-panache)

    • A Vert.x reactive database driver extension (quarkus-reactive-pg-client, quarkus-reactive-mysql-client, …​)

    • One of the Quarkus REST serialization extensions (quarkus-rest-jsonb, quarkus-rest-jackson, …​)

<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 REST Jackson for serialization -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-rest-jackson</artifactId>
    </dependency>
</dependencies>

MongoDB

  • Add the required dependencies to your build file

    • MongoDB REST Data with Panache extension (quarkus-mongodb-rest-data-panache)

    • One of the RESTEasy JSON serialization extensions (quarkus-rest-jackson or quarkus-rest-jsonb)

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

<!-- Use this if you are using Quarkus REST -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-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 Quarkus REST
implementation("io.quarkus:quarkus-rest-jackson")

// Use this if you are going to use RESTEasy Classic
// implementation("io.quarkus:quarkus-resteasy-jackson")

Generating resources

与 Panache 结合使用的 REST Data 会根据应用程序中可用的接口生成 Jakarta REST 资源。对于每一个您要生成的实体和存储库,请提供一个资源接口。_Do not implement these interfaces and don’t provide custom methods because they will be ignored._但是,您可以覆盖扩展接口的方法以对其进行自定义(请参阅结尾部分)。

REST Data with Panache generates Jakarta REST resources based on the interfaces available in your application. For each entity and repository that you want to generate, provide a resource interface. Do not implement these interfaces and don’t provide custom methods because they will be ignored. You can, however, override the methods from the extended interface in order to customize them (see the section at the end).

PanacheEntityResource

如果您的应用程序有扩展 PanacheEntityPanacheEntityBase 类的实体(例如 Person),您可以指示与 Panache 结合使用的 REST Data 利用以下接口为其生成 Jakarta REST 资源:

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

如果您的应用程序有简单实体(例如 Person)和实现 PanacheRepositoryPanacheRepositoryBase 接口的存储库(例如 PersonRepository),则可以指示与 Panache 结合使用的 REST Data 使用以下接口为其生成 Jakarta REST 资源:

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 与 Panache 使用以下接口生成其 Jakarta REST 资源:

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

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

PanacheMongoRepositoryResource

如果您的应用程序有一个简单的实体(例如 Person)和一个实现了 PanacheMongoRepository`或 `PanacheMongoRepositoryBase`接口的存储库(例如 `PersonRepository),您可以指示 REST Data 与 Panache 使用以下接口生成其 Jakarta REST 资源:

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

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

The generated resource

生成的资源对于实体和存储库来说在功能上是等效的。唯一的区别是正在使用的数据访问模式和数据存储。

The generated resources will be functionally equivalent for both entities and repositories. The only difference being the particular data access pattern and data storage in use.

如果您已定义了上面提到的 `PeopleResource`接口之一,此扩展将使用特定数据访问策略生成其实现。然后,已实现的类将由生成的 Jakarta REST 资源使用,该资源看起来像这样:

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);
        }
    }
}

Resource customisation

REST Data 与 Panache 提供了 `@ResourceProperties`和 `@MethodProperties`注释,可用于自定义资源的某些功能。

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);
}

Available options

@ResourceProperties

  • exposed - whether resource could be exposed. A global resource property that can be overridden for each method. Default is true.

  • path - resource base path. Default path is a hyphenated lowercase resource name without a suffix of resource or 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 - whether collection responses should be paged or not. First, last, previous and next page URIs are included in the response headers if they exist. Request page index and size are taken from the page and size query parameters that default to 0 and 20 respectively. Default is true.

  • hal - in addition to the standard application/json responses, generates additional methods that can return application/hal+json responses if requested via an Accept header. Default is false.

  • halCollectionName - name that should be used when generating a hal collection response. Default name is a hyphenated lowercase resource name without a suffix of resource or controller.

@MethodProperties

  • exposed - does not expose a particular HTTP verb when set to false. Default is true.

  • path - operation path (this is appended to the resource base path). Default is an empty string.

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

Adding additional methods to the generated resource

您可以通过向资源接口添加这些方法,用 REST Data 和 Panache 扩展来向生成的资源添加其他方法,例如:

You can add additional methods to the generated resources by the REST Data with Panache extension by adding these methods to the resource interface, for example:

@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`生成的方法一起公开。

And this method will be exposed along with the generated methods using http://localhost:8080/people/name/Johan.

Securing endpoints

REST Data with Panache 将使用 jakarta.annotation.security 包中在资源接口上定义的安全注释:

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,以列出允许访问资源或操作的安全角色。

Additionally, if you are only interested in specifying the roles that are allowed to use the resources, the @ResourceProperties and @MethodProperties annotations have the field rolesAllowed to list the security roles permitted to access the resource or operation.

Query parameters to list entities

REST Data with Panache 支持以下查询参数来获取实体列表:

REST Data with Panache supports the following query parameters to get the list of entities:

  • page - a page number which should be returned by a list operation. It applies to the paged resources only and is a number starting with 0. Default is 0.

  • size - a page size which should be returned by a list operation. It applies to the paged resources only and is a number starting with 1. Default is 20.

  • sort - a comma separated list of fields which should be used for sorting a result of a list operation. Fields are sorted in the ascending order unless they’re prefixed with a -. E.g. ?sort=name,-age will sort the result by the name ascending by the age descending.

  • namedQuery - a named query that should be configured at entity level using the annotation @NamedQuery.

例如,如果您想在第一页中获取两个 People 实体,则应调用 http://localhost:8080/people?page=0&size=2,响应应如下所示:

For example, if you want to get two People entities in the first page, you should call http://localhost:8080/people?page=0&size=2, and the response should look like:

[
  {
    "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 将返回:

Additionally, you can also filter by the entity fields by adding a query param with the name of the field and the value, for example, calling http://localhost:8080/people?name=Peter Peterson would return:

[
  {
    "id": 2,
    "name": "Peter Peterson",
    "birth": "1986-11-20"
  }
]

仅支持按 String、Boolean、Character、Double、Float、Integer、Long、Short、Byte 和基本类型按字段进行筛选。

Filtering by fields is only supported for String, Boolean, Character, Double, Float, Integer, Long, Short, Byte and the primitive types.

Complex filtering to list entities using @NamedQuery

列出实体时,您可以指定一个命名查询以进行筛选。例如,在实体中具有以下命名查询:

You can specify a named query to filter when listing the entities. For example, having the following named query in your entity:

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

在此示例中,我们添加了一个命名查询,以列出 name 字段中包含某些文本的所有人员。

In this example, we have added a named query to list all the persons that contains some text in the name field.

接下来,我们可以在使用生成的资源列出实体时设置一个查询参数 namedQuery,其中包括我们要使用的命名查询的名称,例如,调用 http://localhost:8080/people?namedQuery=Person.containsInName&name=ter 将返回所有姓名包含文本 "ter" 的人。

Next, we can set a query param namedQuery when listing the entities using the generated resource with the name of the named query that we want to use, for example, calling http://localhost:8080/people?namedQuery=Person.containsInName&name=ter would return all the persons which name contains the text "ter".

有关命名查询的工作原理的更多信息,请参阅 Hibernate ORM 指南或 Hibernate Reactive 指南。

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

Resource Method Before/After Listeners

REST Data with Panache 支持订阅以下资源方法钩子:

REST Data with Panache supports the subscription to the following resource method hooks:

  • Before/After add resource

  • Before/After update resource

  • Before/After delete resource

要注册资源方法侦听器,您需要提供一个实现接口 RestDataResourceMethodListener 的 bean,例如:

To register your resource method listener, you need to provide a bean that implements the interface RestDataResourceMethodListener, for example:

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

Response body examples

如上所述,REST Data with Panache 支持 application/jsonapplication/hal+json 响应内容类型。以下是如何为 getlist 操作响应主体的示例,假设数据库中有五个 Person 记录。

As mentioned above REST Data with Panache supports the application/json and application/hal+json response content types. Here are a couple of examples of how a response body would look like for the get and list operations assuming there are five Person records in a database.

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

这两个响应都将包含以下标头:

Both responses would also contain these headers:

将不会包含 previous 链接标头(以及 hal 链接),因为不存在前一页。

A previous link header (and hal link) would not be included, because the previous page does not exist.

Include/Exclude Jakarta REST classes

Using Build time conditions

得益于在构建时的条件,Quarkus 可以直接启用或禁用 Jakarta REST 资源、提供商和特性,与 CDI Bean 相同。因此,可以使用配置文件条件 (@io.quarkus.arc.profile.IfBuildProfile@io.quarkus.arc.profile.UnlessBuildProfile) 和/或属性条件 (io.quarkus.arc.properties.IfBuildPropertyio.quarkus.arc.properties.UnlessBuildProperty) 对 REST 数据和 Panache 接口进行注释,以在构建时向 Quarkus 指示生成的 Jakarta REST 类应在哪些条件下包含。

Quarkus enables the inclusion or exclusion of Jakarta REST Resources, Providers and Features directly thanks to build time conditions in the same that it does for CDI beans. Thus, the REST Data with Panache interfaces can be annotated with profile conditions (@io.quarkus.arc.profile.IfBuildProfile or @io.quarkus.arc.profile.UnlessBuildProfile) and/or with property conditions (io.quarkus.arc.properties.IfBuildProperty or io.quarkus.arc.properties.UnlessBuildProperty) to indicate to Quarkus at build time under which conditions the generated Jakarta REST classes should be included.

在以下示例中,Quarkus 将仅当启用了构建配置文件 app1 时才包含从 PeopleResource 接口生成的资源。

In the following example, Quarkus will include the generated resource from the PeopleResource interface if and only if the build profile app1 has been enabled.

@IfBuildProfile("app1")
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
}

Using a runtime property

仅在使用 Quarkus REST Quarkus 拓展时才可用此选项。

This option is only available when using the Quarkus REST Quarkus extension.

Quarkus 还可基于使用 @io.quarkus.resteasy.reactive.server.EndpointDisabled 注释的运行时属性的值有条件地禁用生成的 Jakarta REST 资源。

Quarkus can also conditionally disable the generated Jakarta REST Resources based on the value of runtime properties using the @io.quarkus.resteasy.reactive.server.EndpointDisabled annotation.

在以下示例中,如果应用程序已将 some.property 配置为 "disable",则 Quarkus 将在运行时从 PeopleResource 接口排除生成的资源。

In the following example, Quarkus will exclude the generated resource from the PeopleResource interface at runtime if the application has some.property configured to "disable".

@EndpointDisabled(name = "some.property", stringValue = "disable")
public interface PeopleResource extends PanacheEntityResource<Person, Long> {
}