Simplified MongoDB with Panache

First: an example

Panache 允许您像这样编写 MongoDB 实体:

public class Person extends PanacheMongoEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Person findByName(String name){
        return find("name", name).firstResult();
    }

    public static List<Person> findAlive(){
        return list("status", Status.Alive);
    }

    public static void deleteLoics(){
        delete("name", "Loïc");
    }
}

您是否注意到与使用 MongoDB API 相比,代码简洁易读了多少?这是否看起来很有趣?请继续读下去!

list() 方法一开始可能令人惊讶。它采用 PanacheQL 查询(JPQL 的子集)片段,并对其余部分进行语境化。这使得代码非常简洁,但仍然易于阅读。MongoDB 本机查询也受支持。

上面描述的基本上是 active record pattern,有时也称为实体模式。MongoDB with Panache 还允许通过 PanacheMongoRepository 使用更经典的 repository pattern

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

解决方案位于 mongodb-panache-quickstart directory

Creating the Maven project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

适用于 Windows 用户:

  • 如果使用 cmd,(不要使用反斜杠 \ ,并将所有内容放在同一行上)

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

此命令将生成一个 Maven 结构,该结构导入 Quarkus REST(以前称为 RESTEasy Reactive)、Jackson 和 MongoDB 以及 Panache 扩展。在此之后,已将 quarkus-mongodb-panache 扩展添加到您的构建文件中。

如果您不想生成一个新项目,请在您的构建文件中添加依赖项:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-mongodb-panache</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-mongodb-panache")

Setting up and configuring MongoDB with Panache

开始:

  • application.properties 中添加您的设置

  • 使您的实体扩展 PanacheMongoEntity(如果您使用储存库模式,则可选)

  • 可以任选使用 @MongoEntity 标注指定系列名称、数据库名称或客户端名称。

然后在 application.properties 中添加相关配置属性。

# configure the MongoDB client for a replica set of two nodes
quarkus.mongodb.connection-string = mongodb://mongo1:27017,mongo2:27017
# mandatory if you don't specify the name of the database using @MongoEntity
quarkus.mongodb.database = person

MongoDB 与 Panache 将使用 quarkus.mongodb.database 属性确定您的实体将保留在哪个数据库中(如果没有被 @MongoEntity 覆盖)。

@MongoEntity 标注允许配置:

  • 多租户应用程序的客户端名称,请参见 Multiple MongoDB Clients。否则将使用默认客户端。

  • 数据库名称,否则将使用 quarkus.mongodb.database 属性或 MongoDatabaseResolver 实施。

  • 系列名称,否则将使用该类的简单名称。

若要对 MongoDB 客户端进行高级配置,您可以遵循 Configuring the MongoDB database guide

Solution 1: using the active record pattern

Defining your entity

若要定义一个 Panache 实体,只需扩展 PanacheMongoEntity 并添加您的列作为公有字段。如果您需要自定义系列名称、数据库或客户端,则可以将 @MongoEntity 标注添加到您的实体中。

@MongoEntity(collection="ThePerson")
public class Person extends PanacheMongoEntity {
    public String name;

    // will be persisted as a 'birth' field in MongoDB
    @BsonProperty("birth")
    public LocalDate birthDate;

    public Status status;
}

使用 @MongoEntity 标注是可选的。在这里,该实体将被存储在 ThePerson 系列中,而不是默认的 Person 系列中。

MongoDB 与 Panache 使用 PojoCodecProvider 将您的实体转换为 MongoDB Document

您将能够使用以下标注来自定义此映射:

  • @BsonId:允许您自定义 ID 字段,请参见 Custom IDs

  • @BsonProperty:自定义该字段的序列化名称。

  • @BsonIgnore:在序列化过程中忽略一个字段。

如果您需要编写访问器,您可以:

public class Person extends PanacheMongoEntity {

    public String name;
    public LocalDate birth;
    public Status status;

    // return name as uppercase in the model
    public String getName(){
        return name.toUpperCase();
    }

    // store all names in lowercase in the DB
    public void setName(String name){
        this.name = name.toLowerCase();
    }
}

感谢我们的字段访问权限重写,当您的用户读取 person.name 时,他们实际上将调用 getName() 访问器,字段写入和设置器也是如此。这允许在运行时正确封装,因为所有字段调用都将被对应的 getter/setter 调用所替换。

Most useful operations

编写实体后,以下是你能够执行的最常见操作:

// creating a person
Person person = new Person();
person.name = "Loïc";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

// persist it: if you keep the default ObjectId ID field, it will be populated by the MongoDB driver
person.persist();

person.status = Status.Dead;

// Your must call update() in order to send your entity modifications to MongoDB
person.update();

// delete it
person.delete();

// getting a list of all Person entities
List<Person> allPersons = Person.listAll();

// finding a specific person by ID
// here we build a new ObjectId, but you can also retrieve it from the existing entity after being persisted
ObjectId personId = new ObjectId(idAsString);
person = Person.findById(personId);

// finding a specific person by ID via an Optional
Optional<Person> optional = Person.findByIdOptional(personId);
person = optional.orElseThrow(() -> new NotFoundException());

// finding all living persons
List<Person> livingPersons = Person.list("status", Status.Alive);

// counting all persons
long countAll = Person.count();

// counting all living persons
long countAlive = Person.count("status", Status.Alive);

// delete all living persons
Person.delete("status", Status.Alive);

// delete all persons
Person.deleteAll();

// delete by id
boolean deleted = Person.deleteById(personId);

// set the name of all living persons to 'Mortal'
long updated = Person.update("name", "Mortal").where("status", Status.Alive);

所有 list 方法都有等效的 stream 版本。

Stream<Person> persons = Person.streamAll();
List<String> namesButEmmanuels = persons
    .map(p -> p.name.toLowerCase() )
    .filter( n -> ! "emmanuel".equals(n) )
    .collect(Collectors.toList());

存在一个 persistOrUpdate() 方法,可在数据库中保存持久性实体或更新实体,它使用 MongoDB 的 upsert 功能在一个查询中执行该操作。

Adding entity methods

在实体本身内部向实体添加自定义查询。这样,你和你同事可以轻松地找到它们,而且查询与操作对象位于同一位置。在你的实体类中将它们作为静态方法添加是 Panache Active Record 的方式。

public class Person extends PanacheMongoEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Person findByName(String name){
        return find("name", name).firstResult();
    }

    public static List<Person> findAlive(){
        return list("status", Status.Alive);
    }

    public static void deleteLoics(){
        delete("name", "Loïc");
    }
}

Solution 2: using the repository pattern

Defining your entity

你可以将实体定义为常规 POJO。如果你需要自定义集合的名称、数据库或客户端,则可以向实体中添加 @MongoEntity 注解。

@MongoEntity(collection="ThePerson")
public class Person  {
    public ObjectId id; // used by MongoDB for the _id field
    public String name;
    public LocalDate birth;
    public Status status;
}

使用 @MongoEntity 标注是可选的。在这里,该实体将被存储在 ThePerson 系列中,而不是默认的 Person 系列中。

含 Panache 的 MongoDB 使用 PojoCodecProvider 将实体转换为 MongoDB Document

您将能够使用以下标注来自定义此映射:

  • @BsonId:允许您自定义 ID 字段,请参见 Custom IDs

  • @BsonProperty:自定义该字段的序列化名称。

  • @BsonIgnore:在序列化过程中忽略一个字段。

你可以使用带有 getter/setter 的公共字段或私有字段。如果你不想自己管理 ID,可以使实体扩展 PanacheMongoEntity

Defining your repository

使用存储库时,你可以通过令其实现 PanacheMongoRepository 来获取与活动记录模式完全相同的便捷方法,注入到存储库中:

@ApplicationScoped
public class PersonRepository implements PanacheMongoRepository<Person> {

   // put your custom logic here as instance methods

   public Person findByName(String name){
       return find("name", name).firstResult();
   }

   public List<Person> findAlive(){
       return list("status", Status.Alive);
   }

   public void deleteLoics(){
       delete("name", "Loïc");
  }
}

`PanacheMongoEntityBase`上定义的所有操作都在存储库上可用,因此使用存储库与使用活动记录模式完全相同,只不过需要注入存储库:

@Inject
PersonRepository personRepository;

@GET
public long count(){
    return personRepository.count();
}

Most useful operations

在编写完存储库后,以下是您将能够执行的最常见操作:

// creating a person
Person person = new Person();
person.name = "Loïc";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

// persist it: if you keep the default ObjectId ID field, it will be populated by the MongoDB driver
personRepository.persist(person);

person.status = Status.Dead;

// Your must call update() in order to send your entity modifications to MongoDB
personRepository.update(person);

// delete it
personRepository.delete(person);

// getting a list of all Person entities
List<Person> allPersons = personRepository.listAll();

// finding a specific person by ID
// here we build a new ObjectId, but you can also retrieve it from the existing entity after being persisted
ObjectId personId = new ObjectId(idAsString);
person = personRepository.findById(personId);

// finding a specific person by ID via an Optional
Optional<Person> optional = personRepository.findByIdOptional(personId);
person = optional.orElseThrow(() -> new NotFoundException());

// finding all living persons
List<Person> livingPersons = personRepository.list("status", Status.Alive);

// counting all persons
long countAll = personRepository.count();

// counting all living persons
long countAlive = personRepository.count("status", Status.Alive);

// delete all living persons
personRepository.delete("status", Status.Alive);

// delete all persons
personRepository.deleteAll();

// delete by id
boolean deleted = personRepository.deleteById(personId);

// set the name of all living persons to 'Mortal'
long updated = personRepository.update("name", "Mortal").where("status", Status.Alive);

所有 list 方法都有等效的 stream 版本。

Stream<Person> persons = personRepository.streamAll();
List<String> namesButEmmanuels = persons
    .map(p -> p.name.toLowerCase() )
    .filter( n -> ! "emmanuel".equals(n) )
    .collect(Collectors.toList());

存在一个 persistOrUpdate() 方法,可在数据库中保存持久性实体或更新实体,它使用 MongoDB 的 upsert 功能在一个查询中执行该操作。

文档的其余部分仅展示基于活动记录模式的用法,但请记住,它们也可以与存储库模式一起执行。为了简洁,存储库模式的示例已被省略。

Writing a Jakarta REST resource

首先,包含 RESTEasy 扩展之一以启用 Jakarta REST 端点,例如,为 Jakarta REST 和 JSON 支持添加 io.quarkus:quarkus-rest-jackson 依赖项。

然后,你可以创建以下资源来创建/读取/更新/删除你的 Person 实体:

@Path("/persons")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class PersonResource {

    @GET
    public List<Person> list() {
        return Person.listAll();
    }

    @GET
    @Path("/{id}")
    public Person get(String id) {
        return Person.findById(new ObjectId(id));
    }

    @POST
    public Response create(Person person) {
        person.persist();
        return Response.created(URI.create("/persons/" + person.id)).build();
    }

    @PUT
    @Path("/{id}")
    public void update(String id, Person person) {
        person.update();
    }

    @DELETE
    @Path("/{id}")
    public void delete(String id) {
        Person person = Person.findById(new ObjectId(id));
        if(person == null) {
            throw new NotFoundException();
        }
        person.delete();
    }

    @GET
    @Path("/search/{name}")
    public Person search(String name) {
        return Person.findByName(name);
    }

    @GET
    @Path("/count")
    public Long count() {
        return Person.count();
    }
}

Advanced Query

Paging

只有当集合包含足够小的数据集时,你才应该使用 liststream 方法。对于较大的数据集,你可以使用 find 方法等效项,该等效项返回一个 PanacheQuery,你可以在其上执行分页:

// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);

// make it use pages of 25 entries at a time
livingPersons.page(Page.ofSize(25));

// get the first page
List<Person> firstPage = livingPersons.list();

// get the second page
List<Person> secondPage = livingPersons.nextPage().list();

// get page 7
List<Person> page7 = livingPersons.page(Page.of(7, 25)).list();

// get the number of pages
int numberOfPages = livingPersons.pageCount();

// get the total number of entities returned by this query without paging
int count = livingPersons.count();

// and you can chain methods of course
return Person.find("status", Status.Alive)
    .page(Page.ofSize(25))
    .nextPage()
    .stream()

PanacheQuery 类型具有许多其他方法来处理分页和返回流。

Using a range instead of pages

PanacheQuery 还允许基于范围的查询。

// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);

// make it use a range: start at index 0 until index 24 (inclusive).
livingPersons.range(0, 24);

// get the range
List<Person> firstRange = livingPersons.list();

// to get the next range, you need to call range again
List<Person> secondRange = livingPersons.range(25, 49).list();

你无法混合范围和页面:如果你使用范围,则所有依赖于存在当前页面的方法都将抛出 UnsupportedOperationException;你可以使用 page(Page)page(int, int) 重新切换到分页。

Sorting

接受查询字符串的所有方法也接受一个可选的 Sort 参数,这允许你抽象化排序:

List<Person> persons = Person.list(Sort.by("name").and("birth"));

// and with more restrictions
List<Person> persons = Person.list("status", Sort.by("name").and("birth"), Status.Alive);

Sort 类具有大量方法,用于添加列和指定排序方向。

Simplified queries

通常,MongoDB 查询具有此种形式: {'firstname': 'John', 'lastname':'Doe'},这在我们称为 MongoDB 原始查询的内容中。

如果你愿意,可以使用它们,但我们还支持我们所称的 PanacheQL,它可以看作是 JPQL (或 HQL) 的子集,并允许你轻松地表达查询。有了含 Panache 的 MongoDB,然后将其映射到 MongoDB 原生查询中。

如果查询不是以 { 开头的,我们将认为它是一个 PanacheQL 查询:

  • @(1) (以及单个参数),将扩展到 @(2)

  • @(3) 将扩展到 @(4),在这里我们将映射 PanacheQL 查询到 MongoDB native 查询形式。我们支持以下运算符,它们将映射到相应的 MongoDB 运算符:'and'、'or'(目前不支持混合使用 'and' 和 'or')、'='、'>'、'>='、'<'、'⇐'、'!= '、'is null'、'is not null',以及映射到 MongoDB @(5) 运算符的 'like'(支持字符串和 JavaScript 模式)。

以下是一些查询示例:

  • @(6) 将映射到 @(7)

  • @(8) 将映射到 @(9)

  • @(10) 将映射到 @(11)。请注意,这将是 @(12) 支持,而不是 SQL 类似模式。

  • @(13) 将映射到 @(14)

  • @(15) 将映射到 @(16)

MongoDB 查询必须是有效的 JSON 文档,在查询中多次使用同一字段不被 PanacheQL 允许,因为它会生成无效的 JSON(请参见 @(17))。

我们还处理一些基本日期类型转换:所有 @(18)、@(19)、@(20) 或 @(21) 类型的字段将使用 @(22) 类型(UTC 日期时间)映射到 @(25)。MongoDB POJO 编解码器不支持 @(23) 和 @(24),因此您应在使用之前转换它们。

Panache 的 MongoDB 还通过提供 @(26) 查询支持扩展的 MongoDB 查询,find/list/stream/count/delete/update 方法支持此查询。

Panache 的 MongoDB 提供了基于更新文档和查询更新多份文档的操作:@(27)。

对于这些操作,您可以用表述查询的方式表述更新文档,以下是一些示例:

  • @(28)(和单个参数),将扩展到更新文档 @(29)

  • @(30) 将映射到更新文档 @(31)

  • @(32) 将映射到更新文档 @(33)

  • @(34) 将映射到更新文档 @(35)

  • @(36) 将映射到更新文档 @(37)

  • @(38) 将按原样使用

Query parameters

您可以按索引传递查询参数(对于本地和 PanacheQL 查询),如下所示:

Person.find("name = ?1 and status = ?2", "Loïc", Status.Alive);
Person.find("{'name': ?1, 'status': ?2}", "Loïc", Status.Alive);

或按名称使用 Map

Map<String, Object> params = new HashMap<>();
params.put("name", "Loïc");
params.put("status", Status.Alive);
Person.find("name = :name and status = :status", params);
Person.find("{'name': :name, 'status', :status}", params);

或者使用简便类 Parameters`照原样使用或构建一个 `Map

// generate a Map
Person.find("name = :name and status = :status",
         Parameters.with("name", "Loïc").and("status", Status.Alive).map());

// use it as-is
Person.find("{'name': :name, 'status': :status}",
         Parameters.with("name", "Loïc").and("status", Status.Alive));

每个查询操作接受按索引 (Object…​) 或按名称 (Map<String,Object>Parameters) 传递参数。

在使用查询参数时,请注意 PanacheQL 查询将引用对象参数名,但本地查询将引用 MongoDB 字段名。

假设以下实体:

public class Person extends PanacheMongoEntity {
    @BsonProperty("lastname")
    public String name;
    public LocalDate birth;
    public Status status;

    public static Person findByNameWithPanacheQLQuery(String name){
        return find("name", name).firstResult();
    }

    public static Person findByNameWithNativeQuery(String name){
        return find("{'lastname': ?1}", name).firstResult();
    }
}

方法 findByNameWithPanacheQLQuery()findByNameWithNativeQuery() 都将返回相同结果,但用 PanacheQL 编写的查询将使用实体字段名称: name,本地查询将使用 MongoDB 字段名称: lastname

Query projection

查询投影可以使用 PanacheQuery 对象上的 project(Class) 方法执行,该对象是由 find() 方法返回的。

您可以使用它来限制数据库将返回哪些字段,ID 字段将始终返回,但不必将其包含在投影类中。

为此,您需要创建仅包含投影字段的类(POJO)。此 POJO 需要用 @ProjectionFor(Entity.class) 注释,其中 Entity 是您的实体类的名称。投影类的字段名或 getter 将用于限制将从数据库加载哪些属性。

PanacheQL 和本地查询均可执行投影。

import io.quarkus.mongodb.panache.common.ProjectionFor;
import org.bson.codecs.pojo.annotations.BsonProperty;

// using public fields
@ProjectionFor(Person.class)
public class PersonName {
    public String name;
}

// using getters
@ProjectionFor(Person.class)
public class PersonNameWithGetter {
    private String name;

    public String getName(){
        return name;
    }

    public void setName(String name){
        this.name = name;
    }
}

// only 'name' will be loaded from the database
PanacheQuery<PersonName> shortQuery = Person.find("status ", Status.Alive).project(PersonName.class);
PanacheQuery<PersonName> query = Person.find("'status': ?1", Status.Alive).project(PersonNameWithGetter.class);
PanacheQuery<PersonName> nativeQuery = Person.find("{'status': 'ALIVE'}", Status.Alive).project(PersonName.class);

无需使用 @BsonProperty 定义自定义列映射,因为将使用来自实体类的映射。

您可以让您的投影类从另一个类扩展。在此情况下,父类也需要使用 @ProjectionFor 注释。

如果您运行 Java 17+,则记录非常适合投影类。

Query debugging

由于使用 Panache 的 MongoDB 允许编写简化查询,因此有时记录生成的本地查询以便进行调试非常方便。

这可以通过在 application.properties 中将以下日志类别设置为 DEBUG 来实现:

quarkus.log.category."io.quarkus.mongodb.panache.common.runtime".level=DEBUG

The PojoCodecProvider: easy object to BSON document conversion.

使用 Panache 的 MongoDB 使用 PojoCodecProviderautomatic POJO support 将您的对象自动转换为 BSON 文档。

如果您遇到 org.bson.codecs.configuration.CodecConfigurationException 异常,则意味着编解码器无法自动转换您的对象。此编解码器遵从 Java Bean 标准,因此它将使用公有字段或 getter/setter 成功转换 POJO。您可以使用 @BsonIgnore 使该编解码器忽略某个字段或 getter/setter。

如果您的类不遵守这些规则(例如包含以 get 开头但不是 setter 的方法),则可以为此类提供自定义编解码器。系统将自动发现您的自定义编解码器并将其注册在编解码器注册表中。参见 Using BSON codec

Transactions

自 4.0 版起,MongoDB 提供 ACID 事务。

要在 Panache 中通过 MongoDB 使用它们,需要使用 `@Transactional`注释来注释启动事务的方法。

在使用 @Transactional`注释的方法内部,如有需要,可以使用 `Panache.getClientSession()`访问 `ClientSession

在 MongoDB 中,事务仅可用于副本集,幸运的是我们的 Dev Services for MongoDB设置了一个单节点副本集,因此它与事务兼容。

Custom IDs

ID 通常是一个敏感主题。在 MongoDB 中,它们通常由具有 `ObjectId`类型的数据库自动生成。在使用 Panache 的 MongoDB 中,ID 由 `org.bson.types.ObjectId`类型的名为 `id`的字段定义,但如果你希望对其进行自定义,我们还有其他方法。

你可以扩展 `PanacheMongoEntityBase`而不是 `PanacheMongoEntity`来指定自己的 ID 策略。然后,你只需通过使用 `@BsonId`对其进行注释来声明所需的 ID 作为公共字段:

@MongoEntity
public class Person extends PanacheMongoEntityBase {

    @BsonId
    public Integer myId;

    //...
}

如果你使用资源库,那么你希望扩展 PanacheMongoRepositoryBase`而不是 `PanacheMongoRepository,并将 ID 类型指定为一个额外的类型参数:

@ApplicationScoped
public class PersonRepository implements PanacheMongoRepositoryBase<Person,Integer> {
    //...
}

使用 `ObjectId`时,MongoDB 会自动为你提供一个值,但如果你使用自定义字段类型,你需要自己提供该值。

如果你希望在 REST 服务中公开 ObjectId`的值,这可能很难使用。因此,我们创建了 Jackson 和 JSON-B 提供程序将其序列化/反序列化为 `String,如果你的项目依赖 Quarkus REST Jackson 扩展或 Quarkus REST JSON-B 扩展,它们将自动注册。

如果你使用标准 `ObjectId`ID 类型,请确保在标识符来自路径参数时,通过创建一个新的 `ObjectId`来检索你的实体。例如:

@GET
@Path("/{id}")
public Person findById(String id) {
    return Person.findById(new ObjectId(id));
}

Working with Kotlin Data classes

Kotlin 数据类是一个定义数据载体类非常便捷的方式,这让它们非常适合定义实体类。

但这种类型的类存在一些限制:所有字段都需要在构造时进行初始化或标记为可为空,并且生成的构造器需要具有作为数据类所有字段的参数。

使用 Panache 的 MongoDB 使用 PojoCodecProvider,它是一个 MongoDB 编解码器,需要存在无参数构造器。

因此,如果你希望将数据类用作实体类,你需要一种方法让 Kotlin 生成一个空构造器。为此,你需要为你的类的所有字段提供默认值。Kotlin 文档中的下列句子对此进行了说明:

On the JVM, if the generated class needs to have a parameterless constructor, default values for all properties have to be specified (see Constructors).

如果由于某种原因上述解决方案被认为不可接受,还有一些其他方法。

首先,你可以创建一个 BSON 编解码器,它将由 Quarkus 自动注册,并将在 `PojoCodecProvider`中使用。参阅文档的这一部分: Using BSON codec

另一个选择是使用 `@BsonCreator`注释来告知 `PojoCodecProvider`使用 Kotlin 数据类默认构造器,在这种情况下,所有构造器参数都必须使用 `@BsonProperty`进行注释:参阅 Supporting pojos without no args constructor

该方法仅在实体扩展 PanacheMongoEntityBase 而非 PanacheMongoEntity 时有效,因为 ID 字段还需要包含在构造器中。

作为一个 Kotlin 数据类定义的 Person 类示例如下:

data class Person @BsonCreator constructor (
    @BsonId var id: ObjectId,
    @BsonProperty("name") var name: String,
    @BsonProperty("birth") var birth: LocalDate,
    @BsonProperty("status") var status: Status
): PanacheMongoEntityBase()

这里我们使用 var,但请注意,还可以使用 val。 为了简洁起见,我们使用 @BsonId 注释,而不是 @BsonProperty("_id"),但无论使用哪一种都是有效的。

最后一个选项是使用 no-arg 编译器插件。此插件由一系列注释配置,最终结果会针对使用这些注释标注的每个类生成无参构造器。

对于使用 Panache 的 MongoDB,您可以为此对数据类使用 @MongoEntity 注释:

@MongoEntity
data class Person (
    var name: String,
    var birth: LocalDate,
    var status: Status
): PanacheMongoEntity()

Reactive Entities and Repositories

使用 Panache 的 MongoDB 允许对实体和存储库使用反应式实现。为此,在定义实体时需要使用 Reactive 变体: ReactivePanacheMongoEntityReactivePanacheMongoEntityBase,在定义存储库时需要使用: ReactivePanacheMongoRepositoryReactivePanacheMongoRepositoryBase

Mutiny

使用 Panache 的 MongoDB 的反应式 API 使用 Mutiny 反应式类型。如果您不熟悉 Mutiny,请查看 Mutiny - an intuitive reactive programming library

Person 类的反应式变体如下:

public class ReactivePerson extends ReactivePanacheMongoEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    // return name as uppercase in the model
    public String getName(){
        return name.toUpperCase();
    }

    // store all names in lowercase in the DB
    public void setName(String name){
        this.name = name.toLowerCase();
    }
}

在反应式变体中,您将可以使用 imperative 变体的相同功能:bson 注释、自定义 ID、PanacheQL,……但是实体或存储库上的方法都会返回反应式类型。

请使用反应式变体查看命令式示例的等效方法:

// creating a person
ReactivePerson person = new ReactivePerson();
person.name = "Loïc";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

// persist it: if you keep the default ObjectId ID field, it will be populated by the MongoDB driver,
// and accessible when uni1 will be resolved
Uni<ReactivePerson> uni1 = person.persist();

person.status = Status.Dead;

// Your must call update() in order to send your entity modifications to MongoDB
Uni<ReactivePerson> uni2 = person.update();

// delete it
Uni<Void> uni3 = person.delete();

// getting a list of all persons
Uni<List<ReactivePerson>> allPersons = ReactivePerson.listAll();

// finding a specific person by ID
// here we build a new ObjectId, but you can also retrieve it from the existing entity after being persisted
ObjectId personId = new ObjectId(idAsString);
Uni<ReactivePerson> personById = ReactivePerson.findById(personId);

// finding a specific person by ID via an Optional
Uni<Optional<ReactivePerson>> optional = ReactivePerson.findByIdOptional(personId);
personById = optional.map(o -> o.orElseThrow(() -> new NotFoundException()));

// finding all living persons
Uni<List<ReactivePerson>> livingPersons = ReactivePerson.list("status", Status.Alive);

// counting all persons
Uni<Long> countAll = ReactivePerson.count();

// counting all living persons
Uni<Long> countAlive = ReactivePerson.count("status", Status.Alive);

// delete all living persons
Uni<Long>  deleteCount = ReactivePerson.delete("status", Status.Alive);

// delete all persons
deleteCount = ReactivePerson.deleteAll();

// delete by id
Uni<Boolean> deleted = ReactivePerson.deleteById(personId);

// set the name of all living persons to 'Mortal'
Uni<Long> updated = ReactivePerson.update("name", "Mortal").where("status", Status.Alive);

如果您将使用 Panache 的 MongoDB 与 Quarkus REST 结合使用,则可以直接在您的 Jakarta REST 资源端点中返回一个反应式类型。

相同的查询功能适用于反应式类型,但 stream() 方法的作用不同:它们返回 Multi (它实现一个反应式流 Publisher,而不是 Stream

它允许更高级的反应式用例,例如,您可以使用它通过 Quarkus REST 发送服务器发送事件 (SSE):

import org.jboss.resteasy.reactive.RestStreamElementType;
import org.reactivestreams.Publisher;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

@GET
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
@RestStreamElementType(MediaType.APPLICATION_JSON)
public Multi<ReactivePerson> streamPersons() {
    return ReactivePerson.streamAll();
}

@RestStreamElementType(MediaType.APPLICATION_JSON) 告诉 Quarkus REST 以 JSON 格式化对象。

Reactive transactions

自 4.0 版起,MongoDB 提供 ACID 事务。

要将它们与反应式实体或存储库配合使用,您需要使用 io.quarkus.mongodb.panache.common.reactive.Panache.withTransaction()

@POST
public Uni<Response> addPerson(ReactiveTransactionPerson person) {
    return Panache.withTransaction(() -> person.persist().map(v -> {
        //the ID is populated before sending it to the database
        String id = person.id.toString();
        return Response.created(URI.create("/reactive-transaction/" + id)).build();
    }));
}

在 MongoDB 中,事务仅可用于副本集,幸运的是我们的 Dev Services for MongoDB设置了一个单节点副本集,因此它与事务兼容。

使用 Panache 的 MongoDB 中的反应式事务支持仍处于实验阶段。

Mocking

Using the active-record pattern

如果您正在使用活动记录模式,则无法直接使用 Mockito,因为它不支持模拟静态方法,但您可以使用 quarkus-panache-mock 模块,该模块允许您使用 Mockito 模拟所有提供的静态方法(包括您自己的)。

将该依赖添加到 pom.xml 中:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-panache-mock</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-panache-mock")

给定此简单实体:

public class Person extends PanacheMongoEntity {

    public String name;

    public static List<Person> findOrdered() {
        return findAll(Sort.by("lastname", "firstname")).list();
    }
}

可以像这样编写模拟测试:

@QuarkusTest
public class PanacheFunctionalityTest {

    @Test
    public void testPanacheMocking() {
        PanacheMock.mock(Person.class);

        // Mocked classes always return a default value
        Assertions.assertEquals(0, Person.count());

        // Now let's specify the return value
        Mockito.when(Person.count()).thenReturn(23L);
        Assertions.assertEquals(23, Person.count());

        // Now let's change the return value
        Mockito.when(Person.count()).thenReturn(42L);
        Assertions.assertEquals(42, Person.count());

        // Now let's call the original method
        Mockito.when(Person.count()).thenCallRealMethod();
        Assertions.assertEquals(0, Person.count());

        // Check that we called it 4 times
        PanacheMock.verify(Person.class, Mockito.times(4)).count();(1)

        // Mock only with specific parameters
        Person p = new Person();
        Mockito.when(Person.findById(12L)).thenReturn(p);
        Assertions.assertSame(p, Person.findById(12L));
        Assertions.assertNull(Person.findById(42L));

        // Mock throwing
        Mockito.when(Person.findById(12L)).thenThrow(new WebApplicationException());
        Assertions.assertThrows(WebApplicationException.class, () -> Person.findById(12L));

        // We can even mock your custom methods
        Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList());
        Assertions.assertTrue(Person.findOrdered().isEmpty());

        PanacheMock.verify(Person.class).findOrdered();
        PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
        PanacheMock.verifyNoMoreInteractions(Person.class);
    }
}
1 请务必在 @3 上而不是 @4 中调用 @2 方法,否则不会知道要传递哪个模拟对象。

Using the repository pattern

如果正在使用仓库模式,则可以使用 @5 模块直接使用 Mockito,这会使模拟 Bean 变得更容易:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-junit5-mockito")

给定此简单实体:

public class Person {

    @BsonId
    public Long id;

    public String name;
}

以及此代码库:

@ApplicationScoped
public class PersonRepository implements PanacheMongoRepository<Person> {
    public List<Person> findOrdered() {
        return findAll(Sort.by("lastname", "firstname")).list();
    }
}

可以像这样编写模拟测试:

@QuarkusTest
public class PanacheFunctionalityTest {
    @InjectMock
    PersonRepository personRepository;

    @Test
    public void testPanacheRepositoryMocking() throws Throwable {
        // Mocked classes always return a default value
        Assertions.assertEquals(0, personRepository.count());

        // Now let's specify the return value
        Mockito.when(personRepository.count()).thenReturn(23L);
        Assertions.assertEquals(23, personRepository.count());

        // Now let's change the return value
        Mockito.when(personRepository.count()).thenReturn(42L);
        Assertions.assertEquals(42, personRepository.count());

        // Now let's call the original method
        Mockito.when(personRepository.count()).thenCallRealMethod();
        Assertions.assertEquals(0, personRepository.count());

        // Check that we called it 4 times
        Mockito.verify(personRepository, Mockito.times(4)).count();

        // Mock only with specific parameters
        Person p = new Person();
        Mockito.when(personRepository.findById(12L)).thenReturn(p);
        Assertions.assertSame(p, personRepository.findById(12L));
        Assertions.assertNull(personRepository.findById(42L));

        // Mock throwing
        Mockito.when(personRepository.findById(12L)).thenThrow(new WebApplicationException());
        Assertions.assertThrows(WebApplicationException.class, () -> personRepository.findById(12L));

        Mockito.when(personRepository.findOrdered()).thenReturn(Collections.emptyList());
        Assertions.assertTrue(personRepository.findOrdered().isEmpty());

        // We can even mock your custom methods
        Mockito.verify(personRepository).findOrdered();
        Mockito.verify(personRepository, Mockito.atLeastOnce()).findById(Mockito.any());
        Mockito.verifyNoMoreInteractions(personRepository);
    }
}

How and why we simplify MongoDB API

在编写 MongoDB 实体时,有很多使人厌烦的事情,用户已经习惯于勉强应付,例如:

  • 重复 ID 逻辑:大多数实体需要 ID,大多数人不关心它是如何设置的,因为它与模型无关。

  • 无意义的 getter 和 setter:由于 Java 缺少对语言中属性的支持,因此我们必须创建字段,然后再为这些字段生成 getter 和 setter,即使它们实际上并未实际执行读取/写入字段之外的任何操作。

  • 传统的 EE 模式建议将实体定义(模型)与其上可执行的操作(DAO、代码库)分开,但实际上,这需要将状态与其操作进行非自然的拆分,尽管我们永远不会对面向对象架构中像普通对象那样的操作执行类似的操作,在面向对象架构中,状态和方法都在同一类中。此外,这需要每个实体有两个类,并且需要注入在需要执行实体操作时使用的 DAO 或代码库,这会中断编辑流程,并要求在返回使用之前退出正在编写的代码以设置注入点。

  • MongoDB 查询非常强大,但对于常见操作来说过于冗长,要求即使不需要所有部分也必须编写查询。

  • MongoDB 查询基于 JSON,因此需要进行一些字符串操作或使用 Document 类型,并且需要大量样板代码。

通过 Panache,我们采取了一个明确的方法来解决所有这些问题:

  • 让实体扩展 PanacheMongoEntity:它有一个自动生成的 ID 字段。如果需要自定义 ID 策略,可以改用扩展 @8 并自行处理 ID。

  • 使用公共字段。摆脱无意义的 getter 和 setter。在内部,我们将生成所有缺少的 getter 和 setter,并重写对这些字段的所有访问以使用访问器方法。这种方式能够在需要时仍然编写 useful 访问器,而即使实体用户仍然使用字段访问,也会使用该访问器。

  • 使用活动记录模式:将所有实体逻辑放入实体类的静态方法中,并且不要创建 DAO。实体超类附带了许多有用的静态方法,并且可以在实体类中添加自己的方法。用户只需键入 Person. 即可开始使用实体 Person,并且在一个地方获取所有操作的完成。

  • 不要编写不需要的查询部分:编写 Person.find("order by name")Person.find("name = ?1 and status = ?2", "Loïc", Status.Alive),甚至更好地编写 Person.find("name", "Loïc")

这就是全部:使用 Panache,MongoDB 的外观从未如此精简和整洁。

Defining entities in external projects or jars

带有 Panache 的 MongoDB 依赖于对实体进行编译时字节码增强。如果在构建 Quarkus 应用程序的同一个项目中定义实体,则一切都将正常工作。

如果实体来自外部项目或 jar,你可以通过添加一个空的 META-INF/beans.xml 文件来确保你的 jar 像 Quarkus 应用程序库一样被处理。

这将允许 Quarkus 索引和增强你的实体,就好像它们在当前项目中一样。

Multitenancy

“多租户是一种软件架构,其中一个软件实例可以服务于多个不同的用户组。软件即服务 (SaaS) 是多租户架构的一个示例。”( Red Hat)。

带有 Panache 的 MongoDB 当前支持基于租户的数据库方法,在与 SQL 数据库进行比较时,它类似于基于模式的租户方法。

Writing the application

为了从传入请求中解决租户并将其映射到特定数据库,您必须创建 io.quarkus.mongodb.panache.common.MongoDatabaseResolver 接口的实现。

import io.quarkus.mongodb.panache.common.MongoDatabaseResolver;
import io.vertx.ext.web.RoutingContext;

@RequestScoped (1)
public class CustomMongoDatabaseResolver implements MongoDatabaseResolver {

    @Inject
    RoutingContext context;

    @Override
    public String resolve() {
        return context.request().getHeader("X-Tenant");
    }

}
1 bean 被制作成 @[19],因为租户解决取决于传入请求。

数据库选择优先级顺序如下:@MongoEntity(database="mizain")MongoDatabaseResolver,然后是 quarkus.mongodb.database 属性。

如果您还使用 OIDC multitenancy ,则如果 OIDC tenantID 和 MongoDBdatabase 相同并且必须从 Vert.x RoutingContext 中提取,您可以将 OIDC TenantResolver 中的 tenant id 作为 RoutingContext 属性传递给带有 Panache MongoDatabaseResolver 的 MongoDB,例如:

import io.quarkus.mongodb.panache.common.MongoDatabaseResolver;
import io.vertx.ext.web.RoutingContext;

@RequestScoped
public class CustomMongoDatabaseResolver implements MongoDatabaseResolver {

    @Inject
    RoutingContext context;
    ...
    @Override
    public String resolve() {
        // OIDC TenantResolver has already calculated the tenant id and saved it as a RoutingContext `tenantId` attribute:
        return context.get("tenantId");
    }
}

由此实体:

import org.bson.codecs.pojo.annotations.BsonId;
import io.quarkus.mongodb.panache.common.MongoEntity;

@MongoEntity(collection = "persons")
public class Person extends PanacheMongoEntityBase {
    @BsonId
    public Long id;
    public String firstname;
    public String lastname;
}

和此资源:

import java.net.URI;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

@Path("/persons")
public class PersonResource {

    @GET
    @Path("/{id}")
    public Person getById(Long id) {
        return Person.findById(id);
    }

    @POST
    public Response create(Person person) {
        Person.persist(person);
        return Response.created(URI.create(String.format("/persons/%d", person.id))).build();
    }
}

根据上述类,我们有足够的资源来持久化和获取来自不同数据库的人员,因此可以了解其工作原理。

Configuring the application

所有租户将同时使用相同的 mongo 连接,因此必须为每个租户创建一个数据库。

quarkus.mongodb.connection-string=mongodb://login:pass@mongo:27017
# The default database
quarkus.mongodb.database=sanjoka

Testing

您可以像这样编写测试:

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Objects;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.Method;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;

@QuarkusTest
public class PanacheMongoMultiTenancyTest {

    public static final String TENANT_HEADER_NAME = "X-Tenant";
    private static final String TENANT_1 = "Tenant1";
    private static final String TENANT_2 = "Tenant2";

    @Test
    public void testMongoDatabaseResolverUsingPersonResource() {
        Person person1 = new Person();
        person1.id = 1L;
        person1.firstname = "Pedro";
        person1.lastname = "Pereira";

        Person person2 = new Person();
        person2.id = 2L;
        person2.firstname = "Tibé";
        person2.lastname = "Venâncio";

        String endpoint = "/persons";

        // creating person 1
        Response createPerson1Response = callCreatePersonEndpoint(endpoint, TENANT_1, person1);
        assertResponse(createPerson1Response, 201);

        // checking person 1 creation
        Response getPerson1ByIdResponse = callGetPersonByIdEndpoint(endpoint, person1.id, TENANT_1);
        assertResponse(getPerson1ByIdResponse, 200, person1);

        // creating person 2
        Response createPerson2Response = callCreatePersonEndpoint(endpoint, TENANT_2, person2);
        assertResponse(createPerson2Response, 201);

        // checking person 2 creation
        Response getPerson2ByIdResponse = callGetPersonByIdEndpoint(endpoint, person2.id, TENANT_2);
        assertResponse(getPerson2ByIdResponse, 200, person2);
    }

    protected Response callCreatePersonEndpoint(String endpoint, String tenant, Object person) {
        return RestAssured.given()
                .header("Content-Type", "application/json")
                .header(TENANT_HEADER_NAME, tenant)
                .body(person)
                .post(endpoint)
                .andReturn();
    }

    private Response callGetPersonByIdEndpoint(String endpoint, Long resourceId, String tenant) {
        RequestSpecification request = RestAssured.given()
                .header("Content-Type", "application/json");

        if (Objects.nonNull(tenant) && !tenant.isBlank()) {
            request.header(TENANT_HEADER_NAME, tenant);
        }

        return request.when()
                .request(Method.GET, endpoint.concat("/{id}"), resourceId)
                .andReturn();
    }

    private void assertResponse(Response response, Integer expectedStatusCode) {
        assertResponse(response, expectedStatusCode, null);
    }

    private void assertResponse(Response response, Integer expectedStatusCode, Object expectedResponseBody) {
        assertEquals(expectedStatusCode, response.statusCode());
        if (Objects.nonNull(expectedResponseBody)) {
            assertTrue(EqualsBuilder.reflectionEquals(response.as(expectedResponseBody.getClass()), expectedResponseBody));
        }
    }
}