Simplified MongoDB with Panache
MongoDB 是一个众所周知且被广泛使用的 NoSQL 数据库,但是使用其原始 API 可能很麻烦,因为您需要将实体和查询表示为 MongoDB Document
。
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
.
MongoDB with Panache 提供了类似于 Hibernate ORM with Panache 中的活动记录风格实体(和仓库),并专注于让您的实体琐碎且易于在 Quarkus 中编写。
MongoDB with Panache provides active record style entities (and repositories) like you have in Hibernate ORM with Panache and focuses on making your entities trivial and fun to write in Quarkus.
它建立在 MongoDB Client 扩展之上。
It is built on top of the MongoDB Client extension.
First: an example
Panache 允许您像这样编写 MongoDB 实体:
Panache allows you to write your MongoDB entities like this:
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 相比,代码简洁易读了多少?这是否看起来很有趣?请继续读下去!
You have noticed how much more compact and readable the code is compared to using the MongoDB API? Does this look interesting? Read on!
|
the |
上面描述的基本上是 active record pattern,有时也称为实体模式。MongoDB with Panache 还允许通过 |
what was described above is essentially the active record pattern, sometimes just called the entity pattern.
MongoDB with Panache also allows for the use of the more classical repository pattern via |
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
克隆 Git 存储库: git clone {quickstarts-clone-url}
,或下载 {quickstarts-archive-url}[存档]。
Clone the Git repository: git clone {quickstarts-clone-url}
, or download an {quickstarts-archive-url}[archive].
解决方案位于 mongodb-panache-quickstart
directory。
The solution is located in the mongodb-panache-quickstart
directory.
Creating the Maven project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
First, we need a new project. Create a new project with the following command:
Unresolved directive in mongodb-panache.adoc - include::{includes}/devtools/create-app.adoc[]
此命令将生成一个 Maven 结构,该结构导入 Quarkus REST(以前称为 RESTEasy Reactive)、Jackson 和 MongoDB 以及 Panache 扩展。在此之后,已将 quarkus-mongodb-panache
扩展添加到您的构建文件中。
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.
如果您不想生成一个新项目,请在您的构建文件中添加依赖项:
If you don’t want to generate a new project, add the dependency in your build file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-panache</artifactId>
</dependency>
implementation("io.quarkus:quarkus-mongodb-panache")
Setting up and configuring MongoDB with Panache
开始:
To get started:
-
add your settings in
application.properties
-
Make your entities extend
PanacheMongoEntity
(optional if you are using the repository pattern) -
Optionally, use the
@MongoEntity
annotation to specify the name of the collection, the name of the database or the name of the client.
然后在 application.properties
中添加相关配置属性。
Then add the relevant configuration properties in 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
覆盖)。
The quarkus.mongodb.database
property will be used by MongoDB with Panache to determine the name of the database where your entities will be persisted (if not overridden by @MongoEntity
).
@MongoEntity
标注允许配置:
The @MongoEntity
annotation allows configuring:
-
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. -
the name of the collection, otherwise the simple name of the class will be used.
若要对 MongoDB 客户端进行高级配置,您可以遵循 Configuring the MongoDB database guide。
For advanced configuration of the MongoDB client, you can follow the Configuring the MongoDB database guide.
Solution 1: using the active record pattern
Defining your entity
若要定义一个 Panache 实体,只需扩展 PanacheMongoEntity
并添加您的列作为公有字段。如果您需要自定义系列名称、数据库或客户端,则可以将 @MongoEntity
标注添加到您的实体中。
To define a Panache entity, simply extend PanacheMongoEntity
and add your columns as public fields.
You can add the @MongoEntity
annotation to your entity if you need to customize the name of the collection, the database, or the client.
@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;
}
使用 |
Annotating with |
MongoDB 与 Panache 使用 PojoCodecProvider 将您的实体转换为 MongoDB Document
。
MongoDB with Panache uses the PojoCodecProvider to convert your entities to a MongoDB Document
.
您将能够使用以下标注来自定义此映射:
You will be allowed to use the following annotations to customize this mapping:
-
@BsonId
: allows you to customize the ID field, see custom-ids. -
@BsonProperty
: customize the serialized name of the field. -
@BsonIgnore
: ignore a field during the serialization.
如果您需要编写访问器,您可以:
If you need to write accessors, you can:
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 调用所替换。
And thanks to our field access rewrite, when your users read person.name
they will actually call your getName()
accessor, and similarly for field writes and the setter.
This allows for proper encapsulation at runtime as all fields calls will be replaced by the corresponding getter/setter calls.
Most useful operations
编写实体后,以下是你能够执行的最常见操作:
Once you have written your entity, here are the most common operations you will be able to perform:
// 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
版本。
All list
methods have equivalent stream
versions.
Stream<Person> persons = Person.streamAll();
List<String> namesButEmmanuels = persons
.map(p -> p.name.toLowerCase() )
.filter( n -> ! "emmanuel".equals(n) )
.collect(Collectors.toList());
存在一个 |
A |
Adding entity methods
在实体本身内部向实体添加自定义查询。这样,你和你同事可以轻松地找到它们,而且查询与操作对象位于同一位置。在你的实体类中将它们作为静态方法添加是 Panache Active Record 的方式。
Add custom queries on your entities inside the entities themselves. That way, you and your co-workers can find them easily, and queries are co-located with the object they operate on. Adding them as static methods in your entity class is the Panache Active Record way.
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
注解。
You can define your entity as regular POJO.
You can add the @MongoEntity
annotation to your entity if you need to customize the name of the collection, the database, or the client.
@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;
}
使用 |
Annotating with |
含 Panache 的 MongoDB 使用 PojoCodecProvider 将实体转换为 MongoDB Document
。
MongoDB with Panache uses the PojoCodecProvider to convert your entities to a MongoDB Document
.
您将能够使用以下标注来自定义此映射:
You will be allowed to use the following annotations to customize this mapping:
-
@BsonId
: allows you to customize the ID field, see custom-ids. -
@BsonProperty
: customize the serialized name of the field. -
@BsonIgnore
: ignore a field during the serialization.
你可以使用带有 getter/setter 的公共字段或私有字段。如果你不想自己管理 ID,可以使实体扩展 |
You can use public fields or private fields with getters/setters.
If you don’t want to manage the ID by yourself, you can make your entity extends |
Defining your repository
使用存储库时,你可以通过令其实现 PanacheMongoRepository
来获取与活动记录模式完全相同的便捷方法,注入到存储库中:
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`上定义的所有操作都在存储库上可用,因此使用存储库与使用活动记录模式完全相同,只不过需要注入存储库:
All the operations that are defined on PanacheMongoEntityBase
are available on your repository, so using it
is exactly the same as using the active record pattern, except you need to inject it:
@Inject
PersonRepository personRepository;
@GET
public long count(){
return personRepository.count();
}
Most useful operations
在编写完存储库后,以下是您将能够执行的最常见操作:
Once you have written your repository, here are the most common operations you will be able to perform:
// 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
版本。
All list
methods have equivalent stream
versions.
Stream<Person> persons = personRepository.streamAll();
List<String> namesButEmmanuels = persons
.map(p -> p.name.toLowerCase() )
.filter( n -> ! "emmanuel".equals(n) )
.collect(Collectors.toList());
存在一个 |
A |
文档的其余部分仅展示基于活动记录模式的用法,但请记住,它们也可以与存储库模式一起执行。为了简洁,存储库模式的示例已被省略。 |
The rest of the documentation show usages based on the active record pattern only, but keep in mind that they can be performed with the repository pattern as well. The repository pattern examples have been omitted for brevity. |
Writing a Jakarta REST resource
首先,包含 RESTEasy 扩展之一以启用 Jakarta REST 端点,例如,为 Jakarta REST 和 JSON 支持添加 io.quarkus:quarkus-rest-jackson
依赖项。
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.
然后,你可以创建以下资源来创建/读取/更新/删除你的 Person 实体:
Then, you can create the following resource to create/read/update/delete your Person entity:
@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
,你可以在其上执行分页:
You should only use list
and stream
methods if your collection contains small enough data sets. For larger data
sets you can use the find
method equivalents, which return a PanacheQuery
on which you can do paging:
// 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
类型具有许多其他方法来处理分页和返回流。
The PanacheQuery
type has many other methods to deal with paging and returning streams.
Using a range instead of pages
PanacheQuery
还允许基于范围的查询。
PanacheQuery
also allows range-based queries.
// 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)
重新切换到分页。
You cannot mix ranges and pages: if you use a range, all methods that depend on having a current page will throw an UnsupportedOperationException
;
you can switch back to paging using page(Page)
or page(int, int)
.
Sorting
接受查询字符串的所有方法也接受一个可选的 Sort
参数,这允许你抽象化排序:
All methods accepting a query string also accept an optional Sort
parameter, which allows you to abstract your sorting:
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
类具有大量方法,用于添加列和指定排序方向。
The Sort
class has plenty of methods for adding columns and specifying sort direction.
Simplified queries
通常,MongoDB 查询具有此种形式: {'firstname': 'John', 'lastname':'Doe'}
,这在我们称为 MongoDB 原始查询的内容中。
Normally, MongoDB queries are of this form: {'firstname': 'John', 'lastname':'Doe'}
, this is what we call MongoDB native queries.
如果你愿意,可以使用它们,但我们还支持我们所称的 PanacheQL,它可以看作是 JPQL (或 HQL) 的子集,并允许你轻松地表达查询。有了含 Panache 的 MongoDB,然后将其映射到 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 查询:
If your query does not start with {
, we will consider it a PanacheQL query:
-
<singlePropertyName>
(and single parameter) which will expand to{'singleColumnName': '?1'}
-
<query>
will expand to{<query>}
where we will map the PanacheQL query to MongoDB native query form. We support the following operators that will be mapped to the corresponding MongoDB operators: 'and', 'or' ( mixing 'and' and 'or' is not currently supported), '=', '>', '>=', '<', '⇐', '!=', 'is null', 'is not null', and 'like' that is mapped to the MongoDB$regex
operator (both String and JavaScript patterns are supported).
以下是一些查询示例:
Here are some query examples:
-
firstname = ?1 and status = ?2
will be mapped to{'firstname': ?1, 'status': ?2}
-
amount > ?1 and firstname != ?2
will be mapped to{'amount': {'$gt': ?1}, 'firstname': {'$ne': ?2}}
-
lastname like ?1
will be mapped to{'lastname': {'$regex': ?1}}
. Be careful that this will be MongoDB regex support and not SQL like pattern. -
lastname is not null
will be mapped to{'lastname':{'$exists': true}}
-
status in ?1
will be mapped to{'status':{$in: [?1]}}
MongoDB 查询必须是有效的 JSON 文档,在查询中多次使用同一字段不被 PanacheQL 允许,因为它会生成无效的 JSON(请参见 @(17))。
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).
我们还处理一些基本日期类型转换:所有 @(18)、@(19)、@(20) 或 @(21) 类型的字段将使用 @(22) 类型(UTC 日期时间)映射到 @(25)。MongoDB POJO 编解码器不支持 @(23) 和 @(24),因此您应在使用之前转换它们。
We also handle some basic date type transformations: all fields of type Date
, LocalDate
, LocalDateTime
or Instant
will be mapped to the
BSON Date using the ISODate
type (UTC datetime).
The MongoDB POJO codec doesn’t support ZonedDateTime
and OffsetDateTime
so you should convert them prior usage.
Panache 的 MongoDB 还通过提供 @(26) 查询支持扩展的 MongoDB 查询,find/list/stream/count/delete/update 方法支持此查询。
MongoDB with Panache also supports extended MongoDB queries by providing a Document
query, this is supported by the find/list/stream/count/delete/update methods.
Panache 的 MongoDB 提供了基于更新文档和查询更新多份文档的操作:@(27)。
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)
.
对于这些操作,您可以用表述查询的方式表述更新文档,以下是一些示例:
For these operations, you can express the update document the same way you express your queries, here are some examples:
-
<singlePropertyName>
(and single parameter) which will expand to the update document{'$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
Query parameters
您可以按索引传递查询参数(对于本地和 PanacheQL 查询),如下所示:
You can pass query parameters, for both native and PanacheQL queries, by index (1-based) as shown below:
Person.find("name = ?1 and status = ?2", "Loïc", Status.Alive);
Person.find("{'name': ?1, 'status': ?2}", "Loïc", Status.Alive);
或按名称使用 Map
:
Or by name using a 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
:
Or using the convenience class Parameters
either as is or to build a 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
) 传递参数。
Every query operation accepts passing parameters by index (Object…
), or by name (Map<String,Object>
or Parameters
).
在使用查询参数时,请注意 PanacheQL 查询将引用对象参数名,但本地查询将引用 MongoDB 字段名。
When you use query parameters, be careful that PanacheQL queries will refer to the Object parameters name but native queries will refer to MongoDB field names.
假设以下实体:
Imagine the following entity:
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
。
Both findByNameWithPanacheQLQuery()
and findByNameWithNativeQuery()
methods will return the same result but query written in PanacheQL
will use the entity field name: name
, and native query will use the MongoDB field name: lastname
.
Query projection
查询投影可以使用 PanacheQuery
对象上的 project(Class)
方法执行,该对象是由 find()
方法返回的。
Query projection can be done with the project(Class)
method on the PanacheQuery
object that is returned by the find()
methods.
您可以使用它来限制数据库将返回哪些字段,ID 字段将始终返回,但不必将其包含在投影类中。
You can use it to restrict which fields will be returned by the database, the ID field will always be returned, but it’s not mandatory to include it inside the projection class.
为此,您需要创建仅包含投影字段的类(POJO)。此 POJO 需要用 @ProjectionFor(Entity.class)
注释,其中 Entity
是您的实体类的名称。投影类的字段名或 getter 将用于限制将从数据库加载哪些属性。
For this, you need to create a class (a POJO) that will only contain the projected fields.
This POJO needs to be annotated with @ProjectionFor(Entity.class)
where Entity
is the name of your entity class.
The field names, or getters, of the projection class will be used to restrict which properties will be loaded from the database.
PanacheQL 和本地查询均可执行投影。
Projection can be done for both PanacheQL and native queries.
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);
无需使用 |
Using |
您可以让您的投影类从另一个类扩展。在此情况下,父类也需要使用 |
You can have your projection class extends from another class. In this case, the parent class also needs to have use |
如果您运行 Java 17+,则记录非常适合投影类。 |
If you run Java 17+, records are a good fit for projection classes. |
Query debugging
由于使用 Panache 的 MongoDB 允许编写简化查询,因此有时记录生成的本地查询以便进行调试非常方便。
As MongoDB with Panache allows writing simplified queries, it is sometimes handy to log the generated native queries for debugging purpose.
这可以通过在 application.properties
中将以下日志类别设置为 DEBUG 来实现:
This can be achieved by setting to DEBUG the following log category inside your application.properties
:
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 文档。
MongoDB with Panache uses the PojoCodecProvider, with automatic POJO support, to automatically convert your object to a BSON document.
如果您遇到 org.bson.codecs.configuration.CodecConfigurationException
异常,则意味着编解码器无法自动转换您的对象。此编解码器遵从 Java Bean 标准,因此它将使用公有字段或 getter/setter 成功转换 POJO。您可以使用 @BsonIgnore
使该编解码器忽略某个字段或 getter/setter。
In case you encounter the org.bson.codecs.configuration.CodecConfigurationException
exception, it means the codec is not able to
automatically convert your object.
This codec obeys the Java Bean standard, so it will successfully convert a POJO using public fields or getters/setters.
You can use @BsonIgnore
to make a field, or a getter/setter, ignored by the codec.
如果您的类不遵守这些规则(例如包含以 get
开头但不是 setter 的方法),则可以为此类提供自定义编解码器。系统将自动发现您的自定义编解码器并将其注册在编解码器注册表中。参见 Using BSON codec。
If your class doesn’t obey these rules (for example by including a method that starts with get
but is not a setter),
you could provide a custom codec for it.
Your custom codec will be automatically discovered and registered inside the codec registry.
See Using BSON codec.
Transactions
自 4.0 版起,MongoDB 提供 ACID 事务。
MongoDB offers ACID transactions since version 4.0.
要在 Panache 中通过 MongoDB 使用它们,需要使用 `@Transactional`注释来注释启动事务的方法。
To use them with MongoDB with Panache you need to annotate the method that starts the transaction with the @Transactional
annotation.
在使用 @Transactional`注释的方法内部,如有需要,可以使用 `Panache.getClientSession()`访问 `ClientSession
。
Inside methods annotated with @Transactional
you can access the ClientSession
with Panache.getClientSession()
if needed.
在 MongoDB 中,事务仅可用于副本集,幸运的是我们的 Dev Services for MongoDB设置了一个单节点副本集,因此它与事务兼容。
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.
Custom IDs
ID 通常是一个敏感主题。在 MongoDB 中,它们通常由具有 `ObjectId`类型的数据库自动生成。在使用 Panache 的 MongoDB 中,ID 由 `org.bson.types.ObjectId`类型的名为 `id`的字段定义,但如果你希望对其进行自定义,我们还有其他方法。
IDs are often a touchy subject. In MongoDB, they are usually auto-generated by the database with an ObjectId
type.
In MongoDB with Panache the ID are defined by a field named id
of the org.bson.types.ObjectId
type,
but if you want to customize them, once again we have you covered.
你可以扩展 `PanacheMongoEntityBase`而不是 `PanacheMongoEntity`来指定自己的 ID 策略。然后,你只需通过使用 `@BsonId`对其进行注释来声明所需的 ID 作为公共字段:
You can specify your own ID strategy by extending PanacheMongoEntityBase
instead of PanacheMongoEntity
. Then
you just declare whatever ID you want as a public field by annotating it by @BsonId
:
@MongoEntity
public class Person extends PanacheMongoEntityBase {
@BsonId
public Integer myId;
//...
}
如果你使用资源库,那么你希望扩展 PanacheMongoRepositoryBase`而不是 `PanacheMongoRepository
,并将 ID 类型指定为一个额外的类型参数:
If you’re using repositories, then you will want to extend PanacheMongoRepositoryBase
instead of PanacheMongoRepository
and specify your ID type as an extra type parameter:
@ApplicationScoped
public class PersonRepository implements PanacheMongoRepositoryBase<Person,Integer> {
//...
}
使用 `ObjectId`时,MongoDB 会自动为你提供一个值,但如果你使用自定义字段类型,你需要自己提供该值。 When using |
如果你希望在 REST 服务中公开 ObjectId`的值,这可能很难使用。因此,我们创建了 Jackson 和 JSON-B 提供程序将其序列化/反序列化为 `String
,如果你的项目依赖 Quarkus REST Jackson 扩展或 Quarkus REST JSON-B 扩展,它们将自动注册。
ObjectId
can be difficult to use if you want to expose its value in your REST service.
So we created Jackson and JSON-B providers to serialize/deserialize them as a String
which are automatically registered if your project depends on either the Quarkus REST Jackson extension or the Quarkus REST JSON-B extension.
如果你使用标准 `ObjectId`ID 类型,请确保在标识符来自路径参数时,通过创建一个新的 `ObjectId`来检索你的实体。例如:
If you use the standard ObjectId
ID type, don’t forget to retrieve your entity by creating a new ObjectId
when the identifier comes from a path parameter. For example:
@GET
@Path("/{id}")
public Person findById(String id) {
return Person.findById(new ObjectId(id));
}
Working with Kotlin Data classes
Kotlin 数据类是一个定义数据载体类非常便捷的方式,这让它们非常适合定义实体类。
Kotlin data classes are a very convenient way of defining data carrier classes, making them a great match to define an entity class.
但这种类型的类存在一些限制:所有字段都需要在构造时进行初始化或标记为可为空,并且生成的构造器需要具有作为数据类所有字段的参数。
But this type of class comes with some limitations: all fields needs to be initialized at construction time or be marked as nullable, and the generated constructor needs to have as parameters all the fields of the data class.
使用 Panache 的 MongoDB 使用 PojoCodecProvider,它是一个 MongoDB 编解码器,需要存在无参数构造器。
MongoDB with Panache uses the PojoCodecProvider, a MongoDB codec which mandates the presence of a parameterless constructor.
因此,如果你希望将数据类用作实体类,你需要一种方法让 Kotlin 生成一个空构造器。为此,你需要为你的类的所有字段提供默认值。Kotlin 文档中的下列句子对此进行了说明:
Therefore, if you want to use a data class as an entity class, you need a way to make Kotlin generate an empty constructor. To do so, you need to provide default values for all the fields of your classes. The following sentence from the Kotlin documentation explains it:
On the JVM, if the generated class needs to have a parameterless constructor, default values for all properties have to be specified (see Constructors).
如果由于某种原因上述解决方案被认为不可接受,还有一些其他方法。
If for whatever reason, the aforementioned solution is deemed unacceptable, there are alternatives.
首先,你可以创建一个 BSON 编解码器,它将由 Quarkus 自动注册,并将在 `PojoCodecProvider`中使用。参阅文档的这一部分: Using BSON codec。
First, you can create a BSON Codec which will be automatically registered by Quarkus and will be used instead of the PojoCodecProvider
.
See this part of the documentation: Using BSON codec.
另一个选择是使用 `@BsonCreator`注释来告知 `PojoCodecProvider`使用 Kotlin 数据类默认构造器,在这种情况下,所有构造器参数都必须使用 `@BsonProperty`进行注释:参阅 Supporting pojos without no args constructor。
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 字段还需要包含在构造器中。
This will only work when the entity extends PanacheMongoEntityBase
and not PanacheMongoEntity
, as the ID field also needs to be included in the constructor.
作为一个 Kotlin 数据类定义的 Person
类示例如下:
An example of a Person
class defined as a Kotlin data class would look like:
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()
这里我们使用 Here we use 为了简洁起见,我们使用 The |
最后一个选项是使用 no-arg 编译器插件。此插件由一系列注释配置,最终结果会针对使用这些注释标注的每个类生成无参构造器。
The last option is to the use the no-arg compiler plugin. This plugin is configured with a list of annotations, and the end result is the generation of no-args constructor for each class annotated with them.
对于使用 Panache 的 MongoDB,您可以为此对数据类使用 @MongoEntity
注释:
For MongoDB with Panache, you could use the @MongoEntity
annotation on your data class for this:
@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
。
MongoDB with Panache allows using reactive style implementation for both entities and repositories.
For this, you need to use the Reactive variants when defining your entities : ReactivePanacheMongoEntity
or ReactivePanacheMongoEntityBase
,
and when defining your repositories: ReactivePanacheMongoRepository
or ReactivePanacheMongoRepositoryBase
.
Mutiny
使用 Panache 的 MongoDB 的反应式 API 使用 Mutiny 反应式类型。如果您不熟悉 Mutiny,请查看 Mutiny - an intuitive reactive programming library。 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
类的反应式变体如下:
The reactive variant of the Person
class will be:
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,……但是实体或存储库上的方法都会返回反应式类型。
You will have access to the same functionalities of the imperative variant inside the reactive one: bson annotations, custom ID, PanacheQL, … But the methods on your entities or repositories will all return reactive types.
请使用反应式变体查看命令式示例的等效方法:
See the equivalent methods from the imperative example with the reactive variant:
// 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 资源端点中返回一个反应式类型。 |
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
。
The same query facility exists for the reactive types, but the stream()
methods act differently: they return a Multi
(which implement a reactive stream Publisher
) instead of a Stream
.
它允许更高级的反应式用例,例如,您可以使用它通过 Quarkus REST 发送服务器发送事件 (SSE):
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();
}
|
|
Reactive transactions
自 4.0 版起,MongoDB 提供 ACID 事务。
MongoDB offers ACID transactions since version 4.0.
要将它们与反应式实体或存储库配合使用,您需要使用 io.quarkus.mongodb.panache.common.reactive.Panache.withTransaction()
。
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();
}));
}
在 MongoDB 中,事务仅可用于副本集,幸运的是我们的 Dev Services for MongoDB设置了一个单节点副本集,因此它与事务兼容。
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.
使用 Panache 的 MongoDB 中的反应式事务支持仍处于实验阶段。
Reactive transaction support inside MongoDB with Panache is still experimental.
Mocking
Using the active-record pattern
如果您正在使用活动记录模式,则无法直接使用 Mockito,因为它不支持模拟静态方法,但您可以使用 quarkus-panache-mock
模块,该模块允许您使用 Mockito 模拟所有提供的静态方法(包括您自己的)。
If you are using the active-record pattern you cannot use Mockito directly as it does not support mocking static methods,
but you can use the quarkus-panache-mock
module which allows you to use Mockito to mock all provided static
methods, including your own.
将该依赖添加到 pom.xml
中:
Add this dependency to your pom.xml
:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-panache-mock")
给定此简单实体:
Given this simple entity:
public class Person extends PanacheMongoEntity {
public String name;
public static List<Person> findOrdered() {
return findAll(Sort.by("lastname", "firstname")).list();
}
}
可以像这样编写模拟测试:
You can write your mocking test like this:
@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 | Be sure to call your verify methods on PanacheMock rather than Mockito , otherwise you won’t know
what mock object to pass. |
Using the repository pattern
如果正在使用仓库模式,则可以使用 @5 模块直接使用 Mockito,这会使模拟 Bean 变得更容易:
If you are using the repository pattern you can use Mockito directly, using the quarkus-junit5-mockito
module,
which makes mocking beans much easier:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-junit5-mockito")
给定此简单实体:
Given this simple entity:
public class Person {
@BsonId
public Long id;
public String name;
}
以及此代码库:
And this repository:
@ApplicationScoped
public class PersonRepository implements PanacheMongoRepository<Person> {
public List<Person> findOrdered() {
return findAll(Sort.by("lastname", "firstname")).list();
}
}
可以像这样编写模拟测试:
You can write your mocking test like this:
@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 实体时,有很多使人厌烦的事情,用户已经习惯于勉强应付,例如:
When it comes to writing MongoDB entities, there are a number of annoying things that users have grown used to reluctantly deal with, such as:
-
Duplicating ID logic: most entities need an ID, most people don’t care how it’s set, because it’s not really relevant to your model.
-
Dumb getters and setters: since Java lacks support for properties in the language, we have to create fields, then generate getters and setters for those fields, even if they don’t actually do anything more than read/write the fields.
-
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 queries are super powerful, but overly verbose for common operations, requiring you to write queries even when you don’t need all the parts.
-
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,我们采取了一个明确的方法来解决所有这些问题:
With Panache, we took an opinionated approach to tackle all these problems:
-
Make your entities extend
PanacheMongoEntity
: it has an ID field that is auto-generated. If you require a custom ID strategy, you can extendPanacheMongoEntityBase
instead and handle the ID yourself. -
Use public fields. Get rid of dumb getter and setters. Under the hood, we will generate all getters and setters that are missing, and rewrite every access to these fields to use the accessor methods. This way you can still write useful accessors when you need them, which will be used even though your entity users still use field accesses.
-
With the active record pattern: put all your entity logic in static methods in your entity class and don’t create DAOs. Your entity superclass comes with lots of super useful static methods, and you can add your own in your entity class. Users can just start using your entity
Person
by typingPerson.
and getting completion for all the operations in a single place. -
Don’t write parts of the query that you don’t need: write
Person.find("order by name")
orPerson.find("name = ?1 and status = ?2", "Loïc", Status.Alive)
or even betterPerson.find("name", "Loïc")
.
这就是全部:使用 Panache,MongoDB 的外观从未如此精简和整洁。
That’s all there is to it: with Panache, MongoDB has never looked so trim and neat.
Defining entities in external projects or jars
带有 Panache 的 MongoDB 依赖于对实体进行编译时字节码增强。如果在构建 Quarkus 应用程序的同一个项目中定义实体,则一切都将正常工作。
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.
如果实体来自外部项目或 jar,你可以通过添加一个空的 META-INF/beans.xml
文件来确保你的 jar 像 Quarkus 应用程序库一样被处理。
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.
这将允许 Quarkus 索引和增强你的实体,就好像它们在当前项目中一样。
This will allow Quarkus to index and enhance your entities as if they were inside the current project.
Multitenancy
“多租户是一种软件架构,其中一个软件实例可以服务于多个不同的用户组。软件即服务 (SaaS) 是多租户架构的一个示例。”( Red Hat)。
"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).
带有 Panache 的 MongoDB 当前支持基于租户的数据库方法,在与 SQL 数据库进行比较时,它类似于基于模式的租户方法。
MongoDB with Panache currently supports the database per tenant approach, it’s similar to schema per tenant approach when compared to SQL databases.
Writing the application
为了从传入请求中解决租户并将其映射到特定数据库,您必须创建 io.quarkus.mongodb.panache.common.MongoDatabaseResolver
接口的实现。
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. |
数据库选择优先级顺序如下:@MongoEntity(database="mizain")
、MongoDatabaseResolver
,然后是 quarkus.mongodb.database
属性。
The database selection priority order is as follow: @MongoEntity(database="mizain")
, MongoDatabaseResolver
,
and then quarkus.mongodb.database
property.
如果您还使用 OIDC multitenancy ,则如果 OIDC tenantID 和 MongoDBdatabase 相同并且必须从 Vert.x 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.
Configuring the application
所有租户将同时使用相同的 mongo 连接,因此必须为每个租户创建一个数据库。
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
Testing
您可以像这样编写测试:
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));
}
}
}