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

使用Panache简化MongoDB

MongoDB is a well known NoSQL Database that is widely used, but using its raw API can be cumbersome as you need to express your entities and your queries as a MongoDB Document.

带有Panache的MongoDB提供了活跃的记录风格的实体(和存储库),就像你在 带有Panache的Hibernate ORM 中所拥有的那样,并专注于使你的实体在Quarkus中编写起来既简单又有趣。

它是建立在 MongoDB客户端 扩展之上的。

第一:一个例子

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

解决方案

我们建议您按照下一节的说明逐步创建应用程序。然而,您可以直接转到已完成的示例。

克隆 Git 仓库。 git clone https://github.com/quarkusio/quarkus-quickstarts.git ,或者下载一个 存档

The solution is located in the mongodb-panache-quickstart directory.

创建Maven项目

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

CLI
quarkus create app org.acme:mongodb-panache-quickstart \
    --extension='rest-jackson,mongodb-panache' \
    --no-code
cd mongodb-panache-quickstart

创建Grade项目,请添加 --gradle 或者 --gradle-kotlin-dsl 参数。

For more information about how to install and use the Quarkus CLI, see the Quarkus CLI guide.

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

创建Grade项目,请添加 -DbuildTool=gradle 或者 -DbuildTool=gradle-kotlin-dsl 参数。

For Windows users:

  • If using cmd, (don’t use backward slash \ and put everything on the same line)

  • If using Powershell, wrap -D parameters in double quotes e.g. "-DprojectArtifactId=mongodb-panache-quickstart"

This command generates a Maven structure importing the Quarkus REST (formerly RESTEasy Reactive) Jackson and MongoDB with Panache extensions. After this, the quarkus-mongodb-panache extension has been added to your build file.

如果你不想生成一个新的项目,可以在你的构建文件中添加该依赖关系:

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

If your project is already configured to use other annotation processors, you will need to additionally add the Panache annotation processor:

pom.xml
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${compiler-plugin.version}</version>
    <configuration>
        <parameters>${maven.compiler.parameters}</parameters>
        <annotationProcessorPaths>
            <!-- Your existing annotation processor(s)... -->
            <path>
                <groupId>io.quarkus</groupId>
                <artifactId>quarkus-panache-common</artifactId>
                <version>${quarkus.platform.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>
build.gradle
annotationProcessor("io.quarkus:quarkus-panache-common")

使用Panache设置和配置MongoDB

起步:

  • 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将使用 quarkus.mongodb.database 属性和Panache来确定实体将持久化的数据库的名称。(如果没有被 @MongoEntity 重写)。

@MongoEntity 注解允许根据以下配置:

  • the name of the client for multitenant application, see Multiple MongoDB Clients. Otherwise, the default client will be used.

  • the name of the database, otherwise the quarkus.mongodb.database property or a MongoDatabaseResolver implementation will be used.

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

对于MongoDB客户端的高级配置,你可以遵循配置 MongoDB数据库指南

解决方案1:使用active record(活动记录)模式

定义你的实体

要定义一个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 uses the PojoCodecProvider to convert your entities to a 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() 方法,类似的还有字段写入和设置器。这允许在运行时进行适当的封装,因为所有字段的调用都将被相应的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 uses the PojoCodecProvider to convert your entities to a MongoDB Document.

你将被允许使用以下注解来自定义这种映射:

  • @BsonId :允许你自定义ID字段,见 自定义ID

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

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

你可以使用公共字段或带有getters/setters的私有字段。如果你不想自己管理ID,你可以让你的实体继承 PanacheMongoEntity

定义你的repository(存储库)

When using Repositories, you can get the exact same convenient methods as with the active record pattern, injected in your Repository, by making them implements 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 上定义的操作都可以在你的版本库上使用,所以使用它与使用active record(活动记录)模式完全一样,只是你需要注入它。

@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 功能在单个查询中完成此操作。
其余的文档只展示了基于活动记录模式的用法,但请记住,这些用法也可以用资源库模式来执行。为了简洁起见,已省略存储库模式示例。

Writing a Jakarta REST resource

First, include one of the RESTEasy extensions to enable Jakarta REST endpoints, for example, add the io.quarkus:quarkus-rest-jackson dependency for Jakarta REST and JSON support.

然后,你可以创建以下资源来create/read/update/delete你的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 类型有许多其他方法来处理分页和返回流。

使用range而不是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();

你不能混合使用ranges和pages:如果你使用range,所有依赖于拥有当前页面的方法将抛出一个 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原生查询。

You can use them if you want, but we also support what we call PanacheQL that can be seen as a subset of JPQL (or HQL) and allows you to easily express a query. MongoDB with Panache will then map it to a MongoDB native query.

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

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

  • <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模式。

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

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

MongoDB queries must be valid JSON documents, using the same field multiple times in a query is not allowed using PanacheQL as it would generate an invalid JSON (see this issue on GitHub).

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

带有Panache的MongoDB还支持扩展的MongoDB查询,提供了一个 Document 查询,这被find/list/stream/count/delete/update方法所支持。

MongoDB with Panache offers operations to update multiple documents based on an update document and a query : Person.update("foo = ?1 and bar = ?2", fooName, barName).where("name = ?1", name).

对于这些操作,你可以用表达查询的同样方式来表达更新文件,这里有一些例子:

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

  • firstname = ?1 and status = ?2 will be mapped to the update document {'$set' : {'firstname': ?1, 'status': ?2}}

  • firstname = :firstname and status = :status will be mapped to the update document {'$set' : {'firstname': :firstname, 'status': :status}}

  • {'firstname' : ?1 and 'status' : ?2} will be mapped to the update document {'$set' : {'firstname': ?1, 'status': ?2}}

  • {'firstname' : :firstname and 'status' : :status} will be mapped to the update document {'$set' : {'firstname': :firstname, 'status': :status}}

  • {'$inc': {'cpt': ?1}} will be used as-is

查询参数

对于原生和PanacheQL查询,你可以通过索引(基于1)传递查询参数,如下所示:

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 注解。
If you run Java 17+, records are a good fit for projection classes.

查询调试

由于带有Panache的MongoDB允许编写简化的查询,有时为调试目的而记录生成的原生查询是很方便的。

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

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

PojoCodecProvider:简单的object(对象)到 BSON 文档的转换。

MongoDB with Panache uses the PojoCodecProvider, with automatic POJO support, to automatically convert your object to a BSON document.

如果你遇到了 org.bson.codecs.configuration.CodecConfigurationException 异常,这意味着编解码器不能自动转换你的对象。这个编解码器遵守Java Bean的标准,所以它将成功地转换使用公共字段或getter/setters的POJO。你可以使用 @BsonIgnore ,使一个字段或一个getter/setter被编解码器所忽略。

如果你的类不遵守这些规则(例如,包括一个以 get 开始但不是setter的方法),你可以为它提供一个自定义的编解码器。你的自定义编解码器将被自动发现并在编解码器注册表内注册。参见 使用BSON编解码器

事务

MongoDB从4.0版本开始提供ACID事务。

要将它们与带有 Panache 的 MongoDB 一起使用,你需要在相应的方法上使用 @Transactional 注解启动事务。

Inside methods annotated with @Transactional you can access the ClientSession with Panache.getClientSession() if needed.

In MongoDB, a transaction is only possible on a replicaset, luckily our Dev Services for MongoDB setups a single node replicaset so it is compatible with transactions.

自定义ID

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会自动为你提供一个值,但如果你使用一个自定义字段类型,你需要自己提供一个值。

ObjectId can be difficult to use if you want to expose its value in your REST service. So we created Jackson and JSON-B providers to serialize/deserialize them as a String which are automatically registered if your project depends on either the Quarkus REST Jackson extension or the Quarkus REST JSON-B extension.

如果你使用标准的 ObjectId ID类型,当标识符来自路径参数时,不要忘记通过创建一个新的 ObjectId 来检索你的实体。例如:

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

使用Kotlin Data classes工作

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

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

MongoDB with Panache uses the PojoCodecProvider, a MongoDB codec which mandates the presence of a parameterless constructor.

因此,如果你想使用一个数据类作为实体类,你需要一种方法来使Kotlin生成一个空的构造函数。要做到这一点,你需要为你的类的所有字段提供默认值。下面这句话来自Kotlin文档的解释:

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

如果由于某种原因,上述解决方案被认为是不可接受的,那么还有其他选择。

首先,你可以创建一个BSON编解码器,它将被Quarkus自动注册,并代替 PojoCodecProvider 。见这部分文档。 使用BSON编解码器

Another option is to use the @BsonCreator annotation to tell the PojoCodecProvider to use the Kotlin data class default constructor, in this case all constructor parameters have to be annotated with @BsonProperty: see 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") ,但使用其中任何一种都是有效的。

最后一个选项是使用 无arg(参数) 编译器插件。这个插件是用一个注释列表来配置的,最终的结果是为每个有注释的类生成无args(参数)构造函数。

对于带有Panache的MongoDB,你可以在你的数据类上使用 @MongoEntity 注解来实现这一点。

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

响应式实体和存储库

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

Mutiny

The reactive API of MongoDB with Panache uses Mutiny reactive types. If you are not familiar with Mutiny, check 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);
If you use MongoDB with Panache in conjunction with Quarkus REST, you can directly return a reactive type inside your Jakarta REST resource endpoint.

响应类型存在相同的查询工具,但 stream() 方法的作用不同:它们返回一个 Multi (实现了反应式流 Publisher ),而不是 Stream

It allows more advanced reactive use cases, for example, you can use it to send server-sent events (SSE) via Quarkus REST:

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) tells Quarkus REST to serialize the object in JSON.

Reactive transactions

MongoDB从4.0版本开始提供ACID事务。

To use them with reactive entities or repositories you need to use 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();
    }));
}

In MongoDB, a transaction is only possible on a replicaset, luckily our Dev Services for MongoDB setups a single node replicaset so it is compatible with transactions.

Reactive transaction support inside MongoDB with Panache is still experimental.

Mock模拟测试

使用active-record模式

如果你使用active-record模式,你不能直接使用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 而不是 Mockito 上调用你的 verify 方法,否则你将不知道要传递什么模拟对象。

使用资源库模式

如果你使用存储库模式,你可以直接使用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,大多数人并不关心它是如何设置的,因为它与你的模型并不真正相关。

  • 繁琐的 getters 和 setters:由于Java语言中缺乏对属性的支持,我们必须创建字段,然后为这些字段getters 和 setters,即使它们除了read/write字段外实际上没有做任何事情。

  • Traditional EE patterns advise to split entity definition (the model) from the operations you can do on them (DAOs, Repositories), but really that requires an unnatural split between the state and its operations even though we would never do something like that for regular objects in the Object-Oriented architecture, where state and methods are in the same class. Moreover, this requires two classes per entity, and requires injection of the DAO or Repository where you need to do entity operations, which breaks your edit flow and requires you to get out of the code you’re writing to set up an injection point before coming back to use it.

  • MongoDB的查询功能超级强大,但对于普通操作来说过于冗长,即使不需要所有的部分,也需要你编写查询。

  • MongoDB queries are JSON based, so you will need some String manipulation or using the Document type, and it will need a lot of boilerplate code.

通过Panache,我们采取了一种有主见的方法来解决所有这些问题:

  • 让你的实体继承 PanacheMongoEntity :它有一个自动生成的ID字段。如果你需要一个自定义的ID策略,你可以继承 PanacheMongoEntityBase ,而不是自己处理ID。

  • 使用公共字段。摆脱繁琐的getter和setters。在后台,我们将生成所有缺失的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 relies on compile-time bytecode enhancements to your entities. If you define your entities in the same project where you build your Quarkus application, everything will work fine.

If the entities come from external projects or jars, you can make sure that your jar is treated like a Quarkus application library by adding an empty META-INF/beans.xml file.

This will allow Quarkus to index and enhance your entities as if they were inside the current project.

Multitenancy

"Multitenancy is a software architecture where a single software instance can serve multiple, distinct user groups. Software-as-a-service (SaaS) offerings are an example of multitenant architecture." (Red Hat).

MongoDB with Panache currently supports the database per tenant approach, it’s similar to schema per tenant approach when compared to SQL databases.

编写应用程序

In order to resolve the tenant from incoming requests and map it to a specific database, you must create an implementation of the io.quarkus.mongodb.panache.common.MongoDatabaseResolver interface.

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 The bean is made @RequestScoped as the tenant resolution depends on the incoming request.

The database selection priority order is as follow: @MongoEntity(database="mizain"), MongoDatabaseResolver, and then quarkus.mongodb.database property.

If you also use OIDC multitenancy, then if the OIDC tenantID and MongoDB database are the same and must be extracted from the Vert.x RoutingContext you can pass the tenant id from the OIDC TenantResolver to the MongoDB with Panache MongoDatabaseResolver as a RoutingContext attribute, for example:

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

Given this entity:

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

And this resource:

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

From the classes above, we have enough to persist and fetch persons from different databases, so it’s possible to see how it works.

配置应用程序

The same mongo connection will be used for all tenants, so a database has to be created for every tenant.

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

测试

You can write your test like this:

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

Related content