简化的 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 本机查询。 |
解决方案
我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。
克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或下载 存档。
解决方案位于 mongodb-panache-quickstart
目录中。
创建 Maven 项目
首先,我们需要一个新项目。使用以下命令创建一个新项目
对于 Windows 用户
-
如果使用 cmd,(不要使用反斜杠
\
并将所有内容放在同一行上) -
如果使用 Powershell,请将
-D
参数用双引号括起来,例如"-DprojectArtifactId=mongodb-panache-quickstart"
此命令生成一个 Maven 结构,导入 Quarkus REST (以前的 RESTEasy Reactive) Jackson 和 MongoDB with Panache 扩展。 之后,quarkus-mongodb-panache
扩展已添加到您的构建文件中。
如果您不想生成新项目,请在您的构建文件中添加依赖项
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-panache</artifactId>
</dependency>
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();
}
}
高级查询
分页
如果您的集合包含足够小的数据集,您应该只使用 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
类型还有许多其他方法来处理分页和返回流。
使用范围而不是页面
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();
您不能混合使用范围和页面:如果您使用范围,则所有依赖于具有当前页面的方法都会抛出 |
排序
所有接受查询字符串的方法也接受可选的 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}} 。 该列表将被正确扩展,带有周围的方括号。 |
我们还处理一些基本日期类型转换:所有类型为 Date
、LocalDate
、LocalDateTime
或 Instant
的字段都将使用 ISODate
类型(UTC datetime)映射到 BSON Date。 MongoDB POJO 编解码器不支持 ZonedDateTime
和 OffsetDateTime
,因此您应该在使用前转换它们。
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> {
//...
}
当使用 |
如果您想在您的 REST 服务中公开其值,ObjectId
可能难以使用。 因此,我们创建了 Jackson 和 JSON-B 提供程序,以将它们序列化/反序列化为 String
,如果您的项目依赖于 Quarkus REST Jackson 扩展或 Quarkus REST JSON-B 扩展,它们会自动注册。
如果您使用标准
|
使用 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()
这里我们使用 为了简洁起见,使用了 |
最后一个选项是使用 no-arg 编译器插件。 此插件配置有一个注解列表,最终结果是为每个用它们注解的类生成无参数构造函数。
对于 MongoDB with Panache,您可以为此在您的数据类上使用 @MongoEntity
注解
@MongoEntity
data class Person (
var name: String,
var birth: LocalDate,
var status: Status
): PanacheMongoEntity()
与 @BsonCreator 方法不同,这里不能使用 |
响应式实体和存储库
MongoDB with Panache 允许为实体和存储库使用响应式风格的实现。 为此,您需要在定义您的实体时使用响应式变体:ReactivePanacheMongoEntity
或 ReactivePanacheMongoEntityBase
,并在定义您的存储库时使用:ReactivePanacheMongoRepository
或 ReactivePanacheMongoRepositoryBase
。
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
<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 | 请务必在 PanacheMock 上调用您的 verify 方法,而不是 Mockito ,否则您将不知道要传递哪个模拟对象。 |
使用存储库模式
如果您使用的是存储库模式,您可以直接使用 Mockito,使用 quarkus-junit5-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);
}
}
我们如何以及为什么简化 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 ,因为租户解析取决于传入的请求。 |
数据库选择优先级顺序如下: |
如果您还使用 OIDC 多租户,那么如果 OIDC tenantID 和 MongoDB 数据库相同,您可以像下面的示例一样从
|
给定此实体
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));
}
}
}