编辑此页面

简化的 MongoDB with Panache

MongoDB 是一个广为人知的 NoSQL 数据库,被广泛使用,但使用其原始 API 可能会很麻烦,因为您需要将实体和查询表示为 MongoDB Document

MongoDB with Panache 提供了类似 Hibernate ORM with Panache 中的活动记录风格实体(和存储库),并专注于使您在 Quarkus 中编写实体变得简单有趣。

它构建在 MongoDB Client 扩展之上。

首先:一个例子

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 本机查询。
上面描述的本质上是 活动记录模式,有时简称为实体模式。 MongoDB with Panache 还允许通过 PanacheMongoRepository 使用更经典的 存储库模式

解决方案

我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。

克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或下载 存档

解决方案位于 mongodb-panache-quickstart 目录中。

创建 Maven 项目

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

CLI(命令行界面)
quarkus create app org.acme:mongodb-panache-quickstart \
    --extension='rest-jackson,mongodb-panache' \
    --no-code
cd mongodb-panache-quickstart

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=mongodb-panache-quickstart \
    -Dextensions='rest-jackson,mongodb-panache' \
    -DnoCode
cd mongodb-panache-quickstart

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

对于 Windows 用户

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

  • 如果使用 Powershell,请将 -D 参数用双引号括起来,例如 "-DprojectArtifactId=mongodb-panache-quickstart"

此命令生成一个 Maven 结构,导入 Quarkus REST (以前的 RESTEasy Reactive) Jackson 和 MongoDB with 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")

设置和配置 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

quarkus.mongodb.database 属性将由 MongoDB with Panache 使用,以确定您的实体将被持久化的数据库名称(如果未被 @MongoEntity 覆盖)。

@MongoEntity 注解允许配置

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

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

  • 集合名称,否则将使用类的简单名称。

有关 MongoDB 客户端的更多高级配置,您可以遵循配置 MongoDB 数据库指南

解决方案 1:使用活动记录模式

定义您的实体

要定义 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 with Panache 使用 PojoCodecProvider 将您的实体转换为 MongoDB Document

您将可以使用以下注解来自定义此映射

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

  • @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() 访问器,字段写入和 setter 也是如此。 这允许在运行时进行适当的封装,因为所有字段调用都将被相应的 getter/setter 调用替换。

最有用的操作

编写实体后,以下是您可以执行的最常见的操作

// 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功能在单个查询中完成。

添加实体方法

在实体本身内部添加实体的自定义查询。 这样,您和您的同事可以轻松找到它们,并且查询与它们操作的对象位于同一位置。 将它们作为静态方法添加到您的实体类中是 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");
    }
}

解决方案 2:使用存储库模式

定义您的实体

您可以将您的实体定义为常规 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 集合中。

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

您将可以使用以下注解来自定义此映射

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

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

  • @BsonIgnore:在序列化期间忽略字段。

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

定义您的存储库

当使用存储库时,通过使它们实现 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();
}

最有用的操作

编写存储库后,以下是您可以执行的最常见的操作

// 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功能在单个查询中完成。
本文档的其余部分仅显示基于活动记录模式的用法,但请记住,它们也可以使用存储库模式执行。 为了简洁起见,存储库模式示例已被省略。

编写 Jakarta REST 资源

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

然后,您可以创建以下资源来创建/读取/更新/删除您的 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();
    }
}

高级查询

分页

如果您的集合包含足够小的数据集,您应该只使用 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 类型还有许多其他方法来处理分页和返回流。

使用范围而不是页面

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) 切换回分页。

排序

所有接受查询字符串的方法也接受可选的 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 类有很多方法可以添加列并指定排序方向。

简化的查询

通常,MongoDB 查询的形式是:{'firstname': 'John', 'lastname':'Doe'},这就是我们所说的 MongoDB 本机查询。

如果您愿意,您可以使用它们,但我们也支持我们称之为 PanacheQL 的东西,它可以被看作是 JPQL (或 HQL) 的子集,它允许您轻松表达一个查询。 然后,MongoDB with Panache 会将其映射到 MongoDB 本机查询。

如果您的查询不以 { 开头,我们将将其视为 PanacheQL 查询

  • <singlePropertyName>(和单个参数)将扩展为 {'singleColumnName': '?1'}

  • <query> 将扩展为 {<query>},我们将在其中将 PanacheQL 查询映射到 MongoDB 本机查询形式。 我们支持以下将被映射到相应 MongoDB 运算符的运算符:“and”、“or”(目前不支持混合“and”和“or”)、“=”、“>”、“>=”、“<”、“<=”、“!=”、“is null”、“is not null”和“like”,它被映射到 MongoDB $regex 运算符(支持字符串和 JavaScript 模式)。

以下是一些查询示例

  • firstname = ?1 and status = ?2 将被映射到 {'firstname': ?1, 'status': ?2}

  • amount > ?1 and firstname != ?2 将被映射到 {'amount': {'$gt': ?1}, 'firstname': {'$ne': ?2}}

  • lastname like ?1 将被映射到 {'lastname': {'$regex': ?1}}。 请注意,这将是 MongoDB regex 支持,而不是 SQL like 模式。

  • lastname is not null 将被映射到 {'lastname':{'$exists': true}}

  • status in ?1 将被映射到 {'status':{$in: ?1}}

MongoDB 查询必须是有效的 JSON 文档,在查询中多次使用相同的字段是不允许使用 PanacheQL 的,因为它会生成无效的 JSON(请参阅 GitHub 上的这个 issue)。
在 Quarkus 3.16 之前,当将 $in 与列表一起使用时,您必须使用 {'status':{$in: [?1]}} 编写您的查询。 从 Quarkus 3.16 开始,请确保您改用 {'status':{$in: ?1}}。 该列表将被正确扩展,带有周围的方括号。

我们还处理一些基本日期类型转换:所有类型为 DateLocalDateLocalDateTimeInstant 的字段都将使用 ISODate 类型(UTC datetime)映射到 BSON Date。 MongoDB POJO 编解码器不支持 ZonedDateTimeOffsetDateTime,因此您应该在使用前转换它们。

MongoDB with Panache 还通过提供 Document 查询来支持扩展的 MongoDB 查询,find/list/stream/count/delete/update 方法支持这一点。

MongoDB with Panache 提供了基于更新文档和查询更新多个文档的操作:Person.update("foo = ?1 and bar = ?2", fooName, barName).where("name = ?1", name)

对于这些操作,您可以像表达查询一样表达更新文档,以下是一些示例

  • <singlePropertyName>(和单个参数)将扩展到更新文档 {'$set' : {'singleColumnName': '?1'}}

  • firstname = ?1 and status = ?2 将被映射到更新文档 {'$set' : {'firstname': ?1, 'status': ?2}}

  • firstname = :firstname and status = :status 将被映射到更新文档 {'$set' : {'firstname': :firstname, 'status': :status}}

  • {'firstname' : ?1 and 'status' : ?2} 将被映射到更新文档 {'$set' : {'firstname': ?1, 'status': ?2}}

  • {'firstname' : :firstname and 'status' : :status} 将被映射到更新文档 {'$set' : {'firstname': :firstname, 'status': :status}}

  • {'$inc': {'cpt': ?1}} 将按原样使用

查询参数

您可以通过索引(从 1 开始)传递查询参数,对于本机查询和 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 查询将引用 Object 参数名称,但本机查询将引用 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

查询投影

可以使用 find() 方法返回的 PanacheQuery 对象上的 project(Class) 方法完成查询投影。

您可以使用它来限制数据库将返回哪些字段,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 注解。
记录非常适合投影类。

查询调试

由于 MongoDB with Panache 允许编写简化的查询,因此有时可以记录生成的本机查询以进行调试。

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

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

PojoCodecProvider:轻松的对象到 BSON 文档转换。

MongoDB with Panache 使用 PojoCodecProvider,使用自动 POJO 支持,自动将您的对象转换为 BSON 文档。 此编解码器还支持 Java 记录,因此您可以将它们用于您的实体或您的实体的属性。

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

如果您的类不遵守这些规则(例如,通过包含一个以 get 开头但不是 setter 的方法),您可以为其提供自定义编解码器。 您的自定义编解码器将被自动发现并在编解码器注册表中注册。 请参阅 使用 BSON 编解码器

事务

MongoDB 自版本 4.0 以来提供 ACID 事务。

要将它们与 MongoDB with Panache 一起使用,您需要使用 @Transactional 注解来注解启动事务的方法。

在用 @Transactional 注解的方法中,如果需要,您可以使用 Panache.getClientSession() 访问 ClientSession

在 MongoDB 中,只有在副本集上才能进行事务,幸运的是,我们的 MongoDB 的 Dev Services 设置了一个单节点副本集,因此它与事务兼容。

自定义 ID

ID 通常是一个棘手的主题。 在 MongoDB 中,它们通常由数据库使用 ObjectId 类型自动生成。 在 MongoDB with Panache 中,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));
}

使用 Kotlin 数据类

Kotlin 数据类是定义数据载体类的一种非常方便的方式,使它们非常适合定义实体类。

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

MongoDB with Panache 使用 PojoCodecProvider,这是一种 MongoDB 编解码器,它强制存在无参数构造函数。

因此,如果您想使用数据类作为实体类,您需要一种方法来使 Kotlin 生成一个空构造函数。 为此,您需要为您的类的所有字段提供默认值。 以下 Kotlin 文档中的句子对此进行了解释

在 JVM 上,如果生成的类需要有一个无参数构造函数,则必须为所有属性指定默认值(请参阅构造函数)。

如果出于任何原因,上述解决方案被认为不可接受,那么还有其他选择。

首先,您可以创建一个 BSON 编解码器,它将被 Quarkus 自动注册,并将被用来代替 PojoCodecProvider。 请参阅本文档的这一部分:使用 BSON 编解码器

另一个选项是使用 @BsonCreator 注解来告诉 PojoCodecProvider 使用 Kotlin 数据类默认构造函数,在这种情况下,所有构造函数参数都必须用 @BsonProperty 注解:请参阅 支持没有无参数构造函数的 pojo

这仅在实体扩展 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 编译器插件。 此插件配置有一个注解列表,最终结果是为每个用它们注解的类生成无参数构造函数。

对于 MongoDB with Panache,您可以为此在您的数据类上使用 @MongoEntity 注解

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

与 @BsonCreator 方法不同,这里不能使用 val。 属性必须定义为 var,否则,系统将为每个属性创建一个具有 null 值的对象。

响应式实体和存储库

MongoDB with Panache 允许为实体和存储库使用响应式风格的实现。 为此,您需要在定义您的实体时使用响应式变体:ReactivePanacheMongoEntityReactivePanacheMongoEntityBase,并在定义您的存储库时使用:ReactivePanacheMongoRepositoryReactivePanacheMongoRepositoryBase

Mutiny

MongoDB with Panache 的响应式 API 使用 Mutiny 响应式类型。 如果您不熟悉 Mutiny,请查看 Mutiny - 一个直观的响应式编程库

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

您将可以访问响应式变体中的命令式变体的相同功能: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);
如果您将 MongoDB with Panache 与 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 格式序列化对象。

响应式事务

MongoDB 自版本 4.0 以来提供 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 中,只有在副本集上才能进行事务,幸运的是,我们的 MongoDB 的 Dev Services 设置了一个单节点副本集,因此它与事务兼容。

MongoDB with Panache 中的响应式事务支持仍处于实验阶段。

模拟

使用活动记录模式

如果您正在使用活动记录模式,您不能直接使用 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 请务必在 PanacheMock 上调用您的 verify 方法,而不是 Mockito,否则您将不知道要传递哪个模拟对象。

使用存储库模式

如果您使用的是存储库模式,您可以直接使用 Mockito,使用 quarkus-junit5-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);
    }
}

我们如何以及为什么简化 MongoDB API

在编写 MongoDB 实体时,用户已经习惯于不情愿地处理许多令人讨厌的事情,例如

  • 复制 ID 逻辑:大多数实体都需要一个 ID,大多数人不在乎如何设置它,因为它与您的模型无关。

  • 愚蠢的 getter 和 setter:由于 Java 在语言中缺乏对属性的支持,我们必须创建字段,然后为这些字段生成 getter 和 setter,即使它们实际上除了读取/写入字段之外什么都不做。

  • 传统的 EE 模式建议将实体定义(模型)与您可以对其执行的操作(DAO、存储库)分开,但实际上这需要在状态和操作之间进行不自然的分割,即使我们永远不会为面向对象架构中的常规对象做这样的事情,其中状态和方法在同一个类中。 此外,这需要每个实体有两个类,并且需要在您需要执行实体操作的地方注入 DAO 或存储库,这会中断您的编辑流程,并要求您退出正在编写的代码以设置一个注入点,然后才能返回使用它。

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

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

使用 Panache,我们采用了自以为是的策略来解决所有这些问题

  • 使您的实体扩展 PanacheMongoEntity:它有一个自动生成的 ID 字段。 如果您需要自定义 ID 策略,您可以改为扩展 PanacheMongoEntityBase 并自己处理 ID。

  • 使用公共字段。 摆脱愚蠢的 getter 和 setter。 在底层,我们将生成所有缺少的 getter 和 setter,并重写对这些字段的每次访问以使用访问器方法。 这样,您仍然可以在需要时编写有用的访问器,即使您的实体用户仍然使用字段访问,也会使用它们。

  • 使用活动记录模式:将您的所有实体逻辑放在您的实体类中的静态方法中,并且不要创建 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 从未如此精简和整洁。

在外部项目或 jar 中定义实体

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

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

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

多租户

"多租户是一种软件架构,其中单个软件实例可以为多个不同的用户组提供服务。 软件即服务 (SaaS) 产品是多租户架构的一个例子。" (Red Hat)。

MongoDB with Panache 目前支持每个租户数据库方法,与 SQL 数据库相比,它类似于每个租户模式方法。

编写应用程序

为了从传入请求中解析租户并将其映射到特定数据库,您必须创建 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 被设置为 @RequestScoped,因为租户解析取决于传入的请求。

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

如果您还使用 OIDC 多租户,那么如果 OIDC tenantID 和 MongoDB 数据库相同,您可以像下面的示例一样从 RoutingContext 属性访问 OIDC 租户 ID

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

@RequestScoped
public class CustomMongoDatabaseResolver implements MongoDatabaseResolver {

    @Inject
    RoutingContext context;
    ...
    @Override
    public String resolve() {
        // OIDC has saved the tenant id as the RoutingContext attribute:
        return context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
    }
}

给定此实体

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

从上面的类中,我们有足够的资源来持久化和从不同的数据库中获取人员,所以有可能看到它是如何工作的。

配置应用程序

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

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

测试

您可以像这样编写您的测试

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

相关内容