使用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本地查询。
|
解决方案
我们建议您按照下一节的说明逐步创建应用程序。然而,您可以直接转到已完成的示例。
克隆 Git 仓库。 git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或者下载一个 存档 。
The solution is located in the mongodb-panache-quickstart
directory.
创建Maven项目
首先,我们需要一个新的项目。用以下命令创建一个新项目:
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.
如果你不想生成一个新的项目,可以在你的构建文件中添加该依赖关系:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-panache</artifactId>
</dependency>
implementation("io.quarkus:quarkus-mongodb-panache")
使用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 aMongoDatabaseResolver
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();
}
}
高级查询
分页
如果你的集合包含足够小的数据集,你应该只使用 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
类型有许多其他方法来处理分页和返回流。
使用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,所有依赖于拥有当前页面的方法将抛出一个 |
排序
所有接受查询字符串的方法也接受一个可选的 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' 映射到 MongoDBregex
运算符(支持字符串和 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
will be mapped to{'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). |
Prior to Quarkus 3.16, when using $in with a list, you had to write your query with {'status':{$in: [?1]}} . Starting with Quarkus 3.16, make sure you use {'status':{$in: ?1}} instead. The list will be properly expanded with surrounding square brackets.
|
我们还处理一些基本的日期类型转换:所有类型为 Date
, LocalDate
, LocalDateTime
或 Instant
的字段都将使用 ISODate
类型(UTC日期时间)映射到 BSON Date 。MongoDB的POJO编解码器不支持 ZonedDateTime
和 OffsetDateTime
,因此你应该在使用前转换它们
带有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 注解。
|
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. This codec also supports Java records so you can use them for your entities or an attribute of your entities.
如果你遇到了 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
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.
如果你使用标准的
|
使用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()
这里我们使用 为了简洁起见,使用了 |
最后一个选项是使用 无arg(参数) 编译器插件。这个插件是用一个注释列表来配置的,最终的结果是为每个有注释的类生成无args(参数)构造函数。
对于带有Panache的MongoDB,你可以在你的数据类上使用 @MongoEntity
注解来实现这一点。
@MongoEntity
data class Person (
var name: String,
var birth: LocalDate,
var status: Status
): PanacheMongoEntity()
响应式实体和存储库
带有Panache的MongoDB允许对实体和存储库使用响应式实现。为此,你需要在定义实体时使用Reactive变形: ReactivePanacheMongoEntity
或 ReactivePanacheMongoEntityBase
,在定义存储库时使用: ReactivePanacheMongoRepository
或 ReactivePanacheMongoRepositoryBase
。
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
中 :
<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 而不是 Mockito 上调用你的 verify 方法,否则你将不知道要传递什么模拟对象。 |
使用资源库模式
如果你使用存储库模式,你可以直接使用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,大多数人并不关心它是如何设置的,因为它与你的模型并不真正相关。
-
繁琐的 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: |
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
|
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));
}
}
}