Simplified MongoDB with Panache
MongoDB 是一个众所周知且被广泛使用的 NoSQL 数据库,但是使用其原始 API 可能很麻烦,因为您需要将实体和查询表示为 MongoDB Document
。
MongoDB with Panache 提供了类似于 Hibernate ORM with Panache 中的活动记录风格实体(和仓库),并专注于让您的实体琐碎且易于在 Quarkus 中编写。
它建立在 MongoDB Client 扩展之上。
- First: an example
- Solution
- Creating the Maven project
- Setting up and configuring MongoDB with Panache
- Solution 1: using the active record pattern
- Solution 2: using the repository pattern
- Writing a Jakarta REST resource
- Advanced Query
- Query debugging
- The PojoCodecProvider: easy object to BSON document conversion.
- Transactions
- Custom IDs
- Working with Kotlin Data classes
- Reactive Entities and Repositories
- Mocking
- How and why we simplify MongoDB API
- Defining entities in external projects or jars
- Multitenancy
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 相比,代码简洁易读了多少?这是否看起来很有趣?请继续读下去!
|
上面描述的基本上是 active record pattern,有时也称为实体模式。MongoDB with Panache 还允许通过 |
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
克隆 Git 存储库: git clone $${quickstarts-base-url}.git
,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。
解决方案位于 mongodb-panache-quickstart
directory。
Creating the Maven project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
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 指南。
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
扩展添加到您的构建文件中。
如果您不想生成一个新项目,请在您的构建文件中添加依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-panache</artifactId>
</dependency>
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;
}
使用 |
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());
存在一个 |
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;
}
使用 |
含 Panache 的 MongoDB 使用 PojoCodecProvider 将实体转换为 MongoDB Document
。
您将能够使用以下标注来自定义此映射:
-
@BsonId
:允许您自定义 ID 字段,请参见 Custom IDs。 -
@BsonProperty
:自定义该字段的序列化名称。 -
@BsonIgnore
:在序列化过程中忽略一个字段。
你可以使用带有 getter/setter 的公共字段或私有字段。如果你不想自己管理 ID,可以使实体扩展 |
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());
存在一个 |
文档的其余部分仅展示基于活动记录模式的用法,但请记住,它们也可以与存储库模式一起执行。为了简洁,存储库模式的示例已被省略。 |
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
只有当集合包含足够小的数据集时,你才应该使用 list
和 stream
方法。对于较大的数据集,你可以使用 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);
无需使用 |
您可以让您的投影类从另一个类扩展。在此情况下,父类也需要使用 |
如果您运行 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 使用 PojoCodecProvider 和 automatic 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()
这里我们使用 |
最后一个选项是使用 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 变体: ReactivePanacheMongoEntity
或 ReactivePanacheMongoEntityBase
,在定义存储库时需要使用: ReactivePanacheMongoRepository
或 ReactivePanacheMongoRepositoryBase
。
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();
}
|
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
中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
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 变得更容易:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
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
|
由此实体:
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));
}
}
}