MongoDB 特定查询方法
你通常对存储库触发的大多数数据访问操作都会导致对 MongoDB 数据库执行查询。定义这样的查询只需在存储库接口上声明一个方法即可,如下例所示:
- 命令式
- 响应式
public interface PersonRepository extends PagingAndSortingRepository<Person, String> {
List<Person> findByLastname(String lastname); 1
Page<Person> findByFirstname(String firstname, Pageable pageable); 2
Person findByShippingAddresses(Address address); 3
Person findFirstByLastname(String lastname); 4
Stream<Person> findAllBy(); 5
}
findByLastname
方法展示了查询所有具有给定姓氏的人。查询是通过解析方法名中的约束条件生成的,这些约束条件可以使用And
和Or
进行连接。因此,方法名生成的查询表达式为{"lastname" : lastname}
。对查询应用分页。你可以在方法签名中添加一个
Pageable
参数,并让方法返回一个Page
实例,Spring Data 会自动对查询进行分页。展示了可以基于非基本类型的属性进行查询。如果找到多个匹配项,则会抛出
IncorrectResultSizeDataAccessException
异常。使用
First
关键字将查询限制为仅返回第一个结果。与 <3> 不同,如果找到多个匹配项,此方法不会抛出异常。使用 Java 8 的
Stream
,在迭代流时读取并转换单个元素。
public interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> {
Flux<Person> findByFirstname(String firstname); 1
Flux<Person> findByFirstname(Publisher<String> firstname); 2
Flux<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable); 3
Mono<Person> findByFirstnameAndLastname(String firstname, String lastname); 4
Mono<Person> findFirstByLastname(String lastname); 5
}
该方法展示了查询所有具有给定
lastname
的人。查询是通过解析方法名中的约束条件生成的,这些约束条件可以使用And
和Or
进行连接。因此,方法名生成的查询表达式为{"lastname" : lastname}
。该方法展示了在给定的
Publisher
发出firstname
后,查询所有具有该firstname
的人。使用
Pageable
将偏移量和排序参数传递给数据库。根据给定条件查找单个实体。如果结果不唯一,则完成时会抛出
IncorrectResultSizeDataAccessException
异常。与 <4> 不同,即使查询产生多个结果文档,也始终发出第一个实体。
响应式仓库不支持 Page
返回类型(如 Mono<Page>
)。
可以在派生的查找器方法中使用 Pageable
,将 sort
、limit
和 offset
参数传递给查询,以减少负载和网络流量。返回的 Flux
将仅发出声明范围内的数据。
Pageable page = PageRequest.of(1, 10, Sort.by("lastname"));
Flux<Person> persons = repository.findByFirstnameOrderByLastname("luke", page);
我们不支持在领域类中引用被映射为 DBRef
的参数。
查询方法支持的关键字
关键词 | 例子 | 逻辑结果 |
---|---|---|
After | findByBirthdateAfter(Date date) | {"birthdate" : {"$gt" : date}} |
GreaterThan | findByAgeGreaterThan(int age) | {"age" : {"$gt" : age}} |
GreaterThanEqual | findByAgeGreaterThanEqual(int age) | {"age" : {"$gte" : age}} |
Before | findByBirthdateBefore(Date date) | {"birthdate" : {"$lt" : date}} |
LessThan | findByAgeLessThan(int age) | {"age" : {"$lt" : age}} |
LessThanEqual | findByAgeLessThanEqual(int age) | {"age" : {"$lte" : age}} |
Between | findByAgeBetween(int from, int to) findByAgeBetween(Range<Integer> range) | {"age" : {"$gt" : from, "$lt" : to}} lower / upper bounds ( $gt / $gte & $lt / $lte ) according to Range |
In | findByAgeIn(Collection ages) | {"age" : {"$in" : [ages…]}} |
NotIn | findByAgeNotIn(Collection ages) | {"age" : {"$nin" : [ages…]}} |
IsNotNull , NotNull | findByFirstnameNotNull() | {"firstname" : {"$ne" : null}} |
IsNull , Null | findByFirstnameNull() | {"firstname" : null} |
Like , StartingWith , EndingWith | findByFirstnameLike(String name) | {"firstname" : name} (name as regex) |
NotLike , IsNotLike | findByFirstnameNotLike(String name) | {"firstname" : { "$not" : name }} (name as regex) |
Containing on String | findByFirstnameContaining(String name) | {"firstname" : name} (name as regex) |
NotContaining on String | findByFirstnameNotContaining(String name) | {"firstname" : { "$not" : name}} (name as regex) |
Containing on Collection | findByAddressesContaining(Address address) | {"addresses" : { "$in" : address}} |
NotContaining on Collection | findByAddressesNotContaining(Address address) | {"addresses" : { "$not" : { "$in" : address}}} |
Regex | findByFirstnameRegex(String firstname) | {"firstname" : {"$regex" : firstname }} |
(No keyword) | findByFirstname(String name) | {"firstname" : name} |
Not | findByFirstnameNot(String name) | {"firstname" : {"$ne" : name}} |
Near | findByLocationNear(Point point) | {"location" : {"$near" : [x,y]}} |
Near | findByLocationNear(Point point, Distance max) | {"location" : {"$near" : [x,y], "$maxDistance" : max}} |
Near | findByLocationNear(Point point, Distance min, Distance max) | {"location" : {"$near" : [x,y], "$minDistance" : min, "$maxDistance" : max}} |
Within | findByLocationWithin(Circle circle) | {"location" : {"$geoWithin" : {"$center" : [ [x, y], distance]}}} |
Within | findByLocationWithin(Box box) | {"location" : {"$geoWithin" : {"$box" : [ [x1, y1], x2, y2]}}} |
IsTrue , True | findByActiveIsTrue() | {"active" : true} |
IsFalse , False | findByActiveIsFalse() | {"active" : false} |
Exists | findByLocationExists(boolean exists) | {"location" : {"$exists" : exists }} |
IgnoreCase | findByUsernameIgnoreCase(String username) | {"username" : {"$regex" : "^username$", "$options" : "i" }} |
如果属性条件比较的是文档,文档中字段的顺序和完全相等性非常重要。
地理空间查询
正如你在前面的关键字表格中所见,有几个关键字会在 MongoDB 查询中触发地理空间操作。Near
关键字允许进一步修改,如下面的几个示例所示。
以下示例展示了如何定义一个 near
查询,用于查找与给定点距离在一定范围内的所有人:
- Imperative
- Reactive
public interface PersonRepository extends MongoRepository<Person, String> {
// { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance}}
List<Person> findByLocationNear(Point location, Distance distance);
}
interface PersonRepository extends ReactiveMongoRepository<Person, String> {
// { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance}}
Flux<Person> findByLocationNear(Point location, Distance distance);
}
在查询方法中添加一个 Distance
参数可以限制结果仅在给定距离范围内。如果 Distance
设置中包含 Metric
,我们会透明地使用 $nearSphere
而不是 $code
,如下例所示:
示例 1. 在 Metrics
中使用 Distance
Point point = new Point(43.7, 48.8);
Distance distance = new Distance(200, Metrics.KILOMETERS);
… = repository.findByLocationNear(point, distance);
// {'location' : {'$nearSphere' : [43.7, 48.8], '$maxDistance' : 0.03135711885774796}}
反应式地理空间存储库查询支持在反应式包装类型中的领域类型和 GeoResult<T>
结果。GeoPage
和 GeoResults
不被支持,因为它们与预先计算平均距离的延迟结果方法相矛盾。不过,你仍然可以传递 Pageable
参数来自行分页结果。
使用 Distance
与 Metric
会导致添加一个 $nearSphere
(而不是普通的 $near
)子句。除此之外,实际距离会根据所使用的 Metrics
进行计算。
(注意,Metric
并不是指公制单位。它可能是英里而不是公里。实际上,metric
指的是测量系统的概念,无论你使用的是哪种系统。)
在目标属性上使用 @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
会强制使用 $nearSphere
操作符。
- Imperative
- Reactive
public interface PersonRepository extends MongoRepository<Person, String> {
// {'geoNear' : 'location', 'near' : [x, y] }
GeoResults<Person> findByLocationNear(Point location);
// No metric: {'geoNear' : 'person', 'near' : [x, y], maxDistance : distance }
// Metric: {'geoNear' : 'person', 'near' : [x, y], 'maxDistance' : distance,
// 'distanceMultiplier' : metric.multiplier, 'spherical' : true }
GeoResults<Person> findByLocationNear(Point location, Distance distance);
// Metric: {'geoNear' : 'person', 'near' : [x, y], 'minDistance' : min,
// 'maxDistance' : max, 'distanceMultiplier' : metric.multiplier,
// 'spherical' : true }
GeoResults<Person> findByLocationNear(Point location, Distance min, Distance max);
// {'geoNear' : 'location', 'near' : [x, y] }
GeoResults<Person> findByLocationNear(Point location);
}
interface PersonRepository extends ReactiveMongoRepository<Person, String> {
// {'geoNear' : 'location', 'near' : [x, y] }
Flux<GeoResult<Person>> findByLocationNear(Point location);
// No metric: {'geoNear' : 'person', 'near' : [x, y], maxDistance : distance }
// Metric: {'geoNear' : 'person', 'near' : [x, y], 'maxDistance' : distance,
// 'distanceMultiplier' : metric.multiplier, 'spherical' : true }
Flux<GeoResult<Person>> findByLocationNear(Point location, Distance distance);
// Metric: {'geoNear' : 'person', 'near' : [x, y], 'minDistance' : min,
// 'maxDistance' : max, 'distanceMultiplier' : metric.multiplier,
// 'spherical' : true }
Flux<GeoResult<Person>> findByLocationNear(Point location, Distance min, Distance max);
// {'geoNear' : 'location', 'near' : [x, y] }
Flux<GeoResult<Person>> findByLocationNear(Point location);
}
基于 JSON 的查询方法与字段限制
通过在存储库查询方法上添加 org.springframework.data.mongodb.repository.Query
注解,你可以指定一个 MongoDB JSON 查询字符串来替代根据方法名派生的查询,如下例所示:
- Imperative
- Reactive
public interface PersonRepository extends MongoRepository<Person, String> {
@Query("{ 'firstname' : ?0 }")
List<Person> findByThePersonsFirstname(String firstname);
}
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {
@Query("{ 'firstname' : ?0 }")
Flux<Person> findByThePersonsFirstname(String firstname);
}
?0
占位符允许你将方法参数中的值替换到 JSON 查询字符串中。
String
类型的参数值在绑定过程中会被转义,这意味着无法通过参数添加 MongoDB 特定的操作符。
你也可以使用 filter
属性来限制映射到 Java 对象中的属性集,如下例所示:
- Imperative
- Reactive
public interface PersonRepository extends MongoRepository<Person, String> {
@Query(value="{ 'firstname' : ?0 }", fields="{ 'firstname' : 1, 'lastname' : 1}")
List<Person> findByThePersonsFirstname(String firstname);
}
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {
@Query(value="{ 'firstname' : ?0 }", fields="{ 'firstname' : 1, 'lastname' : 1}")
Flux<Person> findByThePersonsFirstname(String firstname);
}
前面示例中的查询仅返回 Person
对象的 firstname
、lastname
和 Id
属性。age
属性是一个 java.lang.Integer
类型,未设置其值,因此其值为 null。
基于 JSON 的 SpEL 表达式查询
查询字符串和字段定义可以与 SpEL 表达式一起使用,以在运行时创建动态查询。SpEL 表达式可以提供谓词值,并可以用于通过子文档扩展谓词。
表达式通过一个包含所有参数的数组来暴露方法参数。以下查询使用 [0]
来声明 lastname
的谓词值(这相当于 ?0
参数绑定):
- Imperative
- Reactive
public interface PersonRepository extends MongoRepository<Person, String> {
@Query("{'lastname': ?#{[0]} }")
List<Person> findByQueryWithExpression(String param0);
}
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {
@Query("{'lastname': ?#{[0]} }")
Flux<Person> findByQueryWithExpression(String param0);
}
表达式可以用来调用函数、评估条件以及构建值。当 SpEL 表达式与 JSON 结合使用时,会显示出一种副作用,因为 SpEL 中的类 Map 声明读起来就像 JSON,如下例所示:
- Imperative
- Reactive
public interface PersonRepository extends MongoRepository<Person, String> {
@Query("{'id': ?#{ [0] ? {$exists :true} : [1] }}")
List<Person> findByQueryWithExpressionAndNestedObject(boolean param0, String param1);
}
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {
@Query("{'id': ?#{ [0] ? {$exists :true} : [1] }}")
Flux<Person> findByQueryWithExpressionAndNestedObject(boolean param0, String param1);
}
在查询字符串中使用 SpEL 可以增强查询功能。然而,它们也可能接受大量不需要的参数。确保在将字符串传递给查询之前对其进行清理,以避免创建漏洞或对查询进行不必要的更改。
表达式的支持可以通过 Query SPI 进行扩展:EvaluationContextExtension
和 ReactiveEvaluationContextExtension
。Query SPI 可以提供属性和函数,并可以自定义根对象。扩展在构建查询时,从应用程序上下文中获取,并在 SpEL 评估时使用。以下示例展示了如何使用评估上下文扩展:
- Imperative
- Reactive
public class SampleEvaluationContextExtension extends EvaluationContextExtensionSupport {
@Override
public String getExtensionId() {
return "security";
}
@Override
public Map<String, Object> getProperties() {
return Collections.singletonMap("principal", SecurityContextHolder.getCurrent().getPrincipal());
}
}
public class SampleEvaluationContextExtension implements ReactiveEvaluationContextExtension {
@Override
public String getExtensionId() {
return "security";
}
@Override
public Mono<? extends EvaluationContextExtension> getExtension() {
return Mono.just(new EvaluationContextExtensionSupport() { ... });
}
}
自行引导 MongoRepositoryFactory
不会感知应用程序上下文,并且需要进一步配置以启用查询 SPI 扩展。
响应式查询方法可以利用 org.springframework.data.spel.spi.ReactiveEvaluationContextExtension
。
全文搜索查询
MongoDB 的全文搜索功能是特定于存储的,因此可以在 MongoRepository
中找到,而不是在更通用的 CrudRepository
中。我们需要一个带有全文索引的文档(请参阅“文本索引”了解如何创建全文索引)。
MongoRepository
上的附加方法将 TextCriteria
作为输入参数。除了这些显式方法之外,还可以添加一个 TextCriteria
派生的仓库方法。这些条件会作为额外的 AND
条件添加。一旦实体包含带有 @TextScore
注解的属性,就可以检索文档的全文评分。此外,@TextScore
注解还使得可以根据文档的评分进行排序,如下例所示:
@Document
class FullTextDocument {
@Id String id;
@TextIndexed String title;
@TextIndexed String content;
@TextScore Float score;
}
interface FullTextRepository extends Repository<FullTextDocument, String> {
// Execute a full-text search and define sorting dynamically
List<FullTextDocument> findAllBy(TextCriteria criteria, Sort sort);
// Paginate over a full-text search result
Page<FullTextDocument> findAllBy(TextCriteria criteria, Pageable pageable);
// Combine a derived query with a full-text search
List<FullTextDocument> findByTitleOrderByScoreDesc(String title, TextCriteria criteria);
}
Sort sort = Sort.by("score");
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingAny("spring", "data");
List<FullTextDocument> result = repository.findAllBy(criteria, sort);
criteria = TextCriteria.forDefaultLanguage().matching("film");
Page<FullTextDocument> page = repository.findAllBy(criteria, PageRequest.of(1, 1, sort));
List<FullTextDocument> result = repository.findByTitleOrderByScoreDesc("mongodb", criteria);
聚合方法
存储库层提供了通过注解的存储库查询方法与聚合框架进行交互的方式。与基于 JSON 的查询类似,你可以使用 org.springframework.data.mongodb.repository.Aggregation
注解来定义一个管道。该定义可以包含简单的占位符,如 ?0
,以及SpEL 表达式 ?#{ … }
。
示例 2. 聚合仓库方法
public interface PersonRepository extends CrudRepository<Person, String> {
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
List<PersonAggregate> groupByLastnameAndFirstnames(); 1
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
List<PersonAggregate> groupByLastnameAndFirstnames(Sort sort); 2
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : ?0 } } }")
List<PersonAggregate> groupByLastnameAnd(String property); 3
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : ?0 } } }")
Slice<PersonAggregate> groupByLastnameAnd(String property, Pageable page); 4
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
Stream<PersonAggregate> groupByLastnameAndFirstnamesAsStream(); 5
@Aggregation(pipeline = {
"{ '$match' : { 'lastname' : '?0'} }",
"{ '$project': { _id : 0, firstname : 1, lastname : 1 } }"
})
Stream<PersonAggregate> groupByLastnameAndFirstnamesAsStream(); 6
@Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
SumValue sumAgeUsingValueWrapper(); 7
@Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
Long sumAge(); 8
@Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
AggregationResults<SumValue> sumAgeRaw(); 9
@Aggregation("{ '$project': { '_id' : '$lastname' } }")
List<String> findAllLastnames(); 10
@Aggregation(pipeline = {
"{ $group : { _id : '$author', books: { $push: '$title' } } }",
"{ $out : 'authors' }"
})
void groupAndOutSkippingOutput(); 11
}
public class PersonAggregate {
private @Id String lastname; 2
private List<String> names;
public PersonAggregate(String lastname, List<String> names) {
// ...
}
// Getter / Setter omitted
}
public class SumValue {
private final Long total; // <6> // <8>
public SumValue(Long total) {
// ...
}
// Getter omitted
}
interface PersonProjection {
String getFirstname();
String getLastname();
}
在
Person
集合中使用聚合管道按lastname
分组firstname
,并将结果返回为PersonAggregate
。如果存在
Sort
参数,$sort
会在声明的管道阶段之后追加,以便它仅影响通过所有其他聚合阶段后的最终结果的顺序。因此,Sort
属性会映射到方法返回类型PersonAggregate
,这将Sort.by("lastname")
转换为{ $sort : { '_id', 1 } }
,因为PersonAggregate.lastname
被注解为@Id
。在动态聚合管道中,将
?0
替换为给定的property
值。$skip
、$limit
和$sort
可以通过Pageable
参数传递。与 <2> 相同,这些操作符会追加到管道定义中。接受Pageable
的方法可以返回Slice
以便更轻松地进行分页。聚合方法可以返回基于接口的投影,将结果
org.bson.Document
封装在代理后面,暴露的 getter 方法会委托给文档中的字段。聚合方法可以返回
Stream
,以便直接从底层游标消费结果。确保在消费后关闭流以释放服务器端游标,可以通过调用close()
或通过try-with-resources
实现。将返回单个
Document
的聚合结果映射到所需的SumValue
目标类型的实例。聚合结果如果是仅包含累加结果的单个文档(例如
$sum
),可以直接从结果Document
中提取。为了获得更多控制,你可以考虑将AggregationResult
作为方法返回类型,如 <7> 所示。获取映射到通用目标包装类型
SumValue
或org.bson.Document
的原始AggregationResults
。与 <6> 类似,可以直接从多个结果
Document
中获取单个值。当返回类型为
void
时,跳过$out
阶段的输出。
在某些场景下,聚合操作可能需要额外的选项,例如最大运行时间、额外的日志注释,或者允许临时将数据写入磁盘的权限。使用 @Meta
注解可以通过 maxExecutionTimeMs
、comment
或 allowDiskUse
来设置这些选项。
interface PersonRepository extends CrudRepository<Person, String> {
@Meta(allowDiskUse = true)
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
List<PersonAggregate> groupByLastnameAndFirstnames();
}
或者使用 @Meta
来创建你自己的注解,如下面的示例所示。
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
@Meta(allowDiskUse = true)
@interface AllowDiskUse { }
interface PersonRepository extends CrudRepository<Person, String> {
@AllowDiskUse
@Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
List<PersonAggregate> groupByLastnameAndFirstnames();
}
简单类型的单一结果检查返回的 Document
,并检查以下内容:
-
文档中只有一个条目,返回它。
-
有两个条目,其中一个是
_id
值。返回另一个。 -
返回第一个可分配给返回类型的值。
-
如果以上情况均不适用,则抛出异常。
在使用 @Aggregation
的仓库方法中,不支持 Page
返回类型。不过,你可以使用 Pageable
参数将 $skip
、$limit
和 $sort
添加到管道中,并让方法返回 Slice
。
示例查询
介绍
本章将介绍 Query by Example 的概念,并解释如何使用它。
示例查询(Query by Example,QBE)是一种用户友好的查询技术,具有简单的界面。它允许动态创建查询,并且不需要您编写包含字段名称的查询。实际上,示例查询根本不需要您使用特定于存储的查询语言来编写查询。
本章解释了 Query by Example 的核心概念。这些信息来自 Spring Data Commons 模块。根据你的数据库,字符串匹配支持可能会有所限制。
用法
查询示例(Query by Example)API 由四部分组成:
-
Probe(探针):一个包含填充字段的实际领域对象示例。
-
ExampleMatcher
(示例匹配器):ExampleMatcher
携带了如何匹配特定字段的详细信息。它可以在多个示例中重复使用。 -
Example
(示例):Example
由探针和ExampleMatcher
组成。它用于创建查询。 -
FetchableFluentQuery
(可获取的流式查询):FetchableFluentQuery
提供了一个流式 API,允许对从Example
派生的查询进行进一步的定制。使用流式 API 可以指定查询的排序、投影和结果处理。
按示例查询非常适合以下几种用例:
-
使用一组静态或动态约束查询数据存储。
-
频繁重构领域对象,而不用担心破坏现有查询。
-
独立于底层数据存储 API 工作。
Query by Example 也有几个限制:
-
不支持嵌套或分组的属性约束,例如
firstname = ?0 或 (firstname = ?1 且 lastname = ?2)
。 -
字符串匹配的存储特定支持。根据你的数据库,字符串匹配可以支持字符串的开头/包含/结尾/正则表达式匹配。
-
其他属性类型的精确匹配。
在开始使用 Query by Example 之前,你需要有一个领域对象。首先,为你的仓库创建一个接口,如下例所示:
public class Person {
@Id
private String id;
private String firstname;
private String lastname;
private Address address;
// … getters and setters omitted
}
前面的示例展示了一个简单的领域对象。你可以用它来创建一个 Example
。默认情况下,具有 null
值的字段会被忽略,字符串会使用存储特定的默认值进行匹配。
在“按示例查询”(Query by Example)中,属性的包含性基于其可空性(nullability)。使用基本类型(如 int
、double
等)的属性始终会被包含,除非 ExampleMatcher 忽略了该属性路径。
示例可以通过使用 of
工厂方法或使用 ExampleMatcher 来构建。Example
是不可变的。下面的列表展示了一个简单的示例:
示例 3. 简单示例
Person person = new Person(); 1
person.setFirstname("Dave"); 2
Example<Person> example = Example.of(person); 3
创建域对象的新实例。
设置要查询的属性。
创建
Example
。
你可以通过使用仓库来运行示例查询。为此,让你的仓库接口扩展 QueryByExampleExecutor<T>
。以下列表展示了 QueryByExampleExecutor
接口的摘录:
public interface QueryByExampleExecutor<T> {
<S extends T> S findOne(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example);
// … more functionality omitted.
}
示例匹配器
示例不仅限于默认设置。你可以通过使用 ExampleMatcher
来指定自己的默认设置,包括字符串匹配、空值处理和特定属性的设置,如下例所示:
示例 4. 使用自定义匹配的示例匹配器
Person person = new Person(); 1
person.setFirstname("Dave"); 2
ExampleMatcher matcher = ExampleMatcher.matching() 3
.withIgnorePaths("lastname") 4
.withIncludeNullValues() 5
.withStringMatcher(StringMatcher.ENDING); 6
Example<Person> example = Example.of(person, matcher); 7
创建一个新的领域对象实例。
设置属性。
创建一个
ExampleMatcher
以期望所有值都匹配。即使没有进一步配置,它也可以在此阶段使用。构建一个新的
ExampleMatcher
以忽略lastname
属性路径。构建一个新的
ExampleMatcher
以忽略lastname
属性路径并包含 null 值。构建一个新的
ExampleMatcher
以忽略lastname
属性路径,包含 null 值,并执行后缀字符串匹配。基于领域对象和配置的
ExampleMatcher
创建一个新的Example
。
默认情况下,ExampleMatcher
期望探针上设置的所有值都匹配。如果你想获取匹配任何隐式定义的谓词的结果,请使用 ExampleMatcher.matchingAny()
。
你可以为个别属性(例如 "firstname" 和 "lastname",或者对于嵌套属性,"address.city")指定行为。你可以使用匹配选项和大小写敏感性来调整它,如下例所示:
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", endsWith())
.withMatcher("lastname", startsWith().ignoreCase());
}
另一种配置匹配器选项的方法是使用 lambda 表达式(在 Java 8 中引入)。这种方法创建了一个回调,要求实现者修改匹配器。你无需返回匹配器,因为配置选项保存在匹配器实例中。以下示例展示了一个使用 lambda 表达式的匹配器:
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", match -> match.endsWith())
.withMatcher("firstname", match -> match.startsWith());
}
由 Example
创建的查询使用配置的合并视图。默认匹配设置可以在 ExampleMatcher
级别进行设置,而个别设置可以应用于特定的属性路径。除非显式定义,否则在 ExampleMatcher
上设置的设置会被属性路径设置继承。属性路径上的设置优先级高于默认设置。下表描述了各种 ExampleMatcher
设置的范围:
表 1. ExampleMatcher
设置的范围
设置项 | 作用范围 |
---|---|
空值处理 | ExampleMatcher |
字符串匹配 | ExampleMatcher 和属性路径 |
忽略属性 | 属性路径 |
大小写敏感性 | ExampleMatcher 和属性路径 |
值转换 | 属性路径 |
Fluent API
QueryByExampleExecutor
还提供了一个我们目前为止尚未提及的方法:<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction)
。与其他方法一样,它执行从 Example
派生的查询。然而,通过第二个参数,你可以控制查询执行的一些方面,这些方面在其他情况下是无法动态控制的。你可以通过在第二个参数中调用 FetchableFluentQuery
的各种方法来实现这一点。sortBy
允许你为结果指定排序方式。as
允许你指定希望将结果转换成的类型。project
限制查询的属性。first
、firstValue
、one
、oneValue
、all
、page
、stream
、count
和 exists
定义了获取的结果类型,以及当结果数量超出预期时查询的行为。
Optional<Person> match = repository.findBy(example,
q -> q
.sortBy(Sort.by("lastname").descending())
.first()
);
运行示例
以下示例展示了在使用存储库(在本例中为 Person
对象)时如何通过示例进行查询:
示例 5. 使用仓库进行示例查询
public interface PersonRepository extends QueryByExampleExecutor<Person> {
}
public class PersonService {
@Autowired PersonRepository personRepository;
public List<Person> findPeople(Person probe) {
return personRepository.findAll(Example.of(probe));
}
}
滚动
滚动是一种更细粒度的方法,用于迭代较大的结果集块。滚动包括一个稳定的排序、一种滚动类型(基于偏移量或基于键的滚动)以及结果限制。你可以通过使用属性名称来定义简单的排序表达式,并通过查询派生使用 Top 或 First 关键字 来定义静态的结果限制。你可以将表达式连接起来,将多个条件收集到一个表达式中。
滚动查询返回一个 Window<T>
,允许获取元素的滚动位置以获取下一个 Window<T>
,直到应用程序消耗完整个查询结果。类似于通过获取下一批结果来使用 Java 的 Iterator<List<…>>
,查询结果滚动允许你通过 Window.positionAt(…)
访问 ScrollPosition
。
Window<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", ScrollPosition.offset());
do {
for (User u : users) {
// consume the user
}
// obtain the next Scroll
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
} while (!users.isEmpty() && users.hasNext());
ScrollPosition
用于标识元素在整个查询结果中的确切位置。查询执行时将位置参数视为 独占,结果将从给定位置 之后 开始。ScrollPosition#offset()
和 ScrollPosition#keyset()
是 ScrollPosition
的特殊形式,用于指示滚动操作的起始位置。
上述示例展示了静态排序和限制。你可以定义接受 Sort
对象的查询方法,以定义更复杂的排序顺序或基于每个请求进行排序。类似地,提供 Limit
对象允许你基于每个请求定义动态限制,而不是应用静态限制。更多关于动态排序和限制的信息,请参阅查询方法详情。
WindowIterator
提供了一个实用工具,用于简化跨 Window
的滚动操作,它消除了检查是否存在下一个 Window
的需要,并应用了 ScrollPosition
。
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
.startingAt(ScrollPosition.offset());
while (users.hasNext()) {
User u = users.next();
// consume the user
}
使用偏移量滚动
偏移量滚动(Offset scrolling)与分页(pagination)类似,它使用一个偏移量计数器来跳过一定数量的结果,使数据源只返回从给定偏移量开始的结果。这种简单的机制避免了将大量结果发送到客户端应用程序。然而,大多数数据库在服务器能够返回结果之前,需要将完整的查询结果物化。
示例 6. 在 Repository 查询方法中使用 OffsetScrollPosition
interface UserRepository extends Repository<User, Long> {
Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
}
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
.startingAt(OffsetScrollPosition.initial()); 1
从零偏移开始,以包含位置
0
的元素。
ScollPosition.offset()
和 ScollPosition.offset(0L)
之间存在区别。前者表示滚动操作的开始,不指向任何特定的偏移量,而后者标识结果中的第一个元素(位于位置 0
)。鉴于滚动的 排他性,使用 ScollPosition.offset(0)
会跳过第一个元素,并将其转换为偏移量 1
。
使用键集过滤进行滚动
基于偏移量的查询要求大多数数据库在服务器返回结果之前需要将整个结果具体化。因此,尽管客户端只看到请求结果的一部分,服务器却需要构建完整的结果,这会导致额外的负载。
Keyset-Filtering 方法通过利用数据库的内置功能来检索结果子集,旨在减少单个查询的计算和 I/O 需求。该方法通过将键传递到查询中,有效地修改过滤条件,从而维护一组键以便继续滚动浏览。
Keyset-Filtering 的核心思想是使用稳定的排序顺序开始检索结果。当你想要滚动到下一个数据块时,获取一个 ScrollPosition
,它用于在排序结果中重建位置。ScrollPosition
捕获当前 Window
中最后一个实体的键集。为了执行查询,重建过程会重写条件子句,以包含所有排序字段和主键,从而使数据库能够利用潜在的索引来执行查询。数据库只需从给定的键集位置构造一个更小的结果,而无需完全物化一个大的结果,然后跳过结果直到达到特定的偏移量。
Keyset-Filtering 要求用于排序的键集属性不可为空。由于存储特定的 null
值处理比较运算符的方式以及需要在索引源上运行查询,这一限制是必要的。对可为空的属性进行 Keyset-Filtering 会导致意外的结果。
interface UserRepository extends Repository<User, Long> {
Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
}
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
.startingAt(ScrollPosition.keyset()); 1
从头开始,不应用额外的过滤。
当数据库中包含与排序字段匹配的索引时,Keyset-Filtering 的效果最佳,因此静态排序效果良好。应用 Keyset-Filtering 的滚动查询要求查询返回用于排序的属性,并且这些属性必须在返回的实体中进行映射。
你可以使用接口和 DTO 投影,但请确保包含所有你排序的属性,以避免键集提取失败。
在指定你的 Sort
排序顺序时,只需包含与查询相关的排序属性即可;如果你不希望确保查询结果的唯一性,那么你不需要这样做。键集查询机制会通过包含主键(或复合主键的任何剩余部分)来修正你的排序顺序,以确保每个查询结果都是唯一的。
排序结果
MongoDB 仓库允许使用多种方法来定义排序顺序。让我们来看以下示例:
- Imperative
- Reactive
public interface PersonRepository extends MongoRepository<Person, String> {
List<Person> findByFirstnameSortByAgeDesc(String firstname); 1
List<Person> findByFirstname(String firstname, Sort sort); 2
@Query(sort = "{ age : -1 }")
List<Person> findByFirstname(String firstname); 3
@Query(sort = "{ age : -1 }")
List<Person> findByLastname(String lastname, Sort sort); 4
}
从方法名派生的静态排序。
SortByAgeDesc
结果为排序参数生成{ age : -1 }
。使用方法参数的动态排序。
Sort.by(DESC, "age")
为排序参数创建{ age : -1 }
。通过
Query
注解实现的静态排序。排序参数按照sort
属性中的定义应用。通过
Query
注解的默认排序与通过方法参数的动态排序结合。Sort.unsorted()
结果为{ age : -1 }
。使用Sort.by(ASC, "age")
会覆盖默认值并创建{ age : 1 }
。Sort.by (ASC, "firstname")
会修改默认值并生成{ age : -1, firstname : 1 }
。
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {
Flux<Person> findByFirstnameSortByAgeDesc(String firstname);
Flux<Person> findByFirstname(String firstname, Sort sort);
@Query(sort = "{ age : -1 }")
Flux<Person> findByFirstname(String firstname);
@Query(sort = "{ age : -1 }")
Flux<Person> findByLastname(String lastname, Sort sort);
}
索引提示
@Hint
注解允许覆盖 MongoDB 的默认索引选择,并强制数据库使用指定的索引。
示例 7. 索引提示的示例
@Hint("lastname-idx") 1
List<Person> findByLastname(String lastname);
@Query(value = "{ 'firstname' : ?0 }", hint = "firstname-idx") 2
List<Person> findByFirstname(String firstname);
使用名为
lastname-idx
的索引。@Query
注解定义了hint
别名,相当于添加了@Hint
注解。
有关索引创建的更多信息,请参考集合管理部分。
排序规则支持
在通用排序规则支持旁边,存储库允许为各种操作定义排序规则。
public interface PersonRepository extends MongoRepository<Person, String> {
@Query(collation = "en_US") 1
List<Person> findByFirstname(String firstname);
@Query(collation = "{ 'locale' : 'en_US' }") 2
List<Person> findPersonByFirstname(String firstname);
@Query(collation = "?1") 3
List<Person> findByFirstname(String firstname, Object collation);
@Query(collation = "{ 'locale' : '?1' }") 4
List<Person> findByFirstname(String firstname, String collation);
List<Person> findByFirstname(String firstname, Collation collation); 5
@Query(collation = "{ 'locale' : 'en_US' }")
List<Person> findByFirstname(String firstname, @Nullable Collation collation); 6
}
静态排序规则定义结果为
{ 'locale' : 'en_US' }
。静态排序规则定义结果为
{ 'locale' : 'en_US' }
。动态排序规则依赖于第 2 个方法参数。允许的类型包括
String
(例如 'en_US')、Locale
(例如 Locale.US)和Document
(例如 new Document("locale", "en_US"))。动态排序规则依赖于第 2 个方法参数。
将
Collation
方法参数应用到查询中。如果
Collation
方法参数不为空,则会覆盖@Query
中的默认collation
。
如果你为仓库查找器方法启用了自动索引创建,那么在创建索引时,将会包含一个潜在的静态排序定义,如 (1) 和 (2) 所示。
最具体的 Collation
会覆盖其他可能定义的规则。这意味着方法的参数会覆盖查询方法注解,查询方法注解会覆盖领域类型注解。
为了简化代码库中整理属性的使用,也可以使用 @Collation
注解,它作为上述提到的注解的元注解。相同的规则和位置适用,此外,直接使用 @Collation
会取代在 @Query
和其他注解上定义的任何整理值。这意味着,如果通过 @Query
和 @Collation
同时声明了整理,那么将选择 @Collation
中的值。
示例 8. 使用 @Collation
@Collation("en_US") 1
class Game {
// ...
}
interface GameRepository extends Repository<Game, String> {
@Collation("en_GB") 2
List<Game> findByTitle(String title);
@Collation("de_AT") 3
@Query(collation="en_GB")
List<Game> findByDescriptionContaining(String keyword);
}
替代
@Document(collation=…)
。替代
@Query(collation=…)
。优先使用
@Collation
而非元数据用法。
读取偏好
@ReadPreference
注解允许你配置 MongoDB 的 ReadPreferences。
示例 9. 读取偏好示例
@ReadPreference("primaryPreferred") 1
public interface PersonRepository extends CrudRepository<Person, String> {
@ReadPreference("secondaryPreferred") 2
List<Person> findWithReadPreferenceAnnotationByLastname(String lastname);
@Query(readPreference = "nearest") 3
List<Person> findWithReadPreferenceAtTagByFirstname(String firstname);
List<Person> findWithReadPreferenceAtTagByFirstname(String firstname); 4
为所有没有查询级别定义的仓库操作(包括继承的、非自定义实现的)配置读取偏好。因此,在这种情况下,读取偏好模式将为
primaryPreferred
使用注解
ReadPreference
中定义的读取偏好模式,在这种情况下为secondaryPreferred
@Query
注解定义了read preference mode
别名,这相当于添加了@ReadPreference
注解。此查询将使用仓库中定义的读取偏好模式。
MongoOperations
和 Query
API 为 ReadPreference
提供了更细粒度的控制。