MongoDB 特定的查询方法

在存储库上触发的多数数据访问操作最终都会导致对 MongoDB 数据库执行查询。定义此类查询是声明存储库接口上方法的问题,如下例所示

具有查询方法的 PersonRepository
  • 命令式

  • 响应式

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)
}
1 findByLastname 方法显示了对具有给定姓氏的所有人员的查询。通过解析方法名称以查找可与 AndOr 连接的约束来派生查询。因此,方法名称生成 {"lastname" : lastname} 的查询表达式。
2 对查询应用分页。您可以使用 Pageable 参数装备方法签名,并让方法返回 Page 实例,Spring Data 会自动相应地对查询进行分页。
3 显示您可以基于非原始类型的属性进行查询。如果找到多个匹配项,则抛出 IncorrectResultSizeDataAccessException
4 使用 First 关键字将查询限制为仅第一个结果。与 <3> 不同,如果找到多个匹配项,此方法不会引发异常。
5 使用 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)
}
1 该方法显示了具有给定 lastname 的所有人的查询。该查询是通过解析方法名称来派生的,该方法名称用于可以与 AndOr 连接的约束。因此,方法名称生成查询表达式 {"lastname" : lastname}
2 该方法显示了具有给定 firstname 的所有人的查询,一旦给定的 Publisher 发出 firstname
3 使用 Pageable 将偏移量和排序参数传递给数据库。
4 为给定条件查找单个实体。它在非唯一结果上完成 IncorrectResultSizeDataAccessException
5 除非 <4>,否则即使查询产生更多结果文档,也会始终发出第一个实体。
反应式存储库不支持 Page 返回类型(如 Mono<Page>)。

可以在派生查找器方法中使用 Pageable,将 sortlimitoffset 参数传递给查询以减少负载和网络流量。返回的 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}}
根据Range的上下界($gt / $gte$lt / $lte

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作为正则表达式)

NotLike, IsNotLike

findByFirstnameNotLike(String name)

{"firstname" : { "$not" : name }}(name作为正则表达式)

字符串上的Containing

findByFirstnameContaining(String name)

{"firstname" : name}(name作为正则表达式)

字符串上的NotContaining

findByFirstnameNotContaining(String name)

{"firstname" : { "$not" : name}}(name作为正则表达式)

集合上的Containing

findByAddressesContaining(Address address)

{"addresses" : { "$in" : address}}

集合上的NotContaining

findByAddressesNotContaining(Address address)

{"addresses" : { "$not" : { "$in" : address}}}

Regex

findByFirstnameRegex(String firstname)

{"firstname" : {"$regex" : firstname }}

(无关键字)

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 查询,该查询查找与给定点距离给定的所有人员

高级 Near 查询
  • 命令式

  • 响应式

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. 使用 DistanceMetrics
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> 结果。不支持 GeoPageGeoResults,因为它们与使用预先计算平均距离的延迟结果方法相矛盾。但是,您仍然可以自己传入一个 Pageable 参数来分页结果。

使用带有 MetricDistance 会导致添加一个 $nearSphere(而不是一个普通的 $near)子句。除此之外,实际距离将根据所使用的 Metrics 进行计算。

(请注意,Metric 并不指度量单位。它可能是英里而不是公里。相反,metric 指的是度量系统的概念,无论您使用哪种系统。)

在目标属性上使用 @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) 会强制使用 $nearSphere 运算符。
  • 命令式

  • 响应式

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 查询字符串以供使用,而不是从方法名称派生查询,如下例所示

  • 命令式

  • 响应式

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 对象的属性集,如下例所示

  • 命令式

  • 响应式

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 对象的 firstnamelastnameId 属性。age 属性(java.lang.Integer)未设置,因此其值为 null。

带有 SpEL 表达式的基于 JSON 的查询

查询字符串和字段定义可以与 SpEL 表达式一起使用,以在运行时创建动态查询。SpEL 表达式可以提供谓词值,并且可以用于使用子文档扩展谓词。

表达式通过包含所有参数的数组公开方法参数。以下查询使用 [0]lastname 声明谓词值(相当于 ?0 参数绑定)

  • 命令式

  • 响应式

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);
}

表达式可用于调用函数、评估条件和构造值。与 JSON 结合使用的 SpEL 表达式会显示副作用,因为 SpEL 中的类 Map 声明就像 JSON,如下例所示

  • 命令式

  • 响应式

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 可以是增强查询的有力方法。但是,它们还可以接受各种不需要的参数。务必在将字符串传递给查询之前对其进行清理,以避免创建漏洞或对查询进行不需要的更改。

表达式支持可通过查询 SPI 扩展:EvaluationContextExtensionReactiveEvaluationContextExtension 查询 SPI 可以提供属性和函数,并可以自定义根对象。在构建查询时,在 SpEL 评估时从应用程序上下文中检索扩展。以下示例显示如何使用评估上下文扩展

  • 命令式

  • 响应式

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("{ $group : { _id : null, total : { $sum : $age } } }")
  SumValue sumAgeUsingValueWrapper();                                              (6)

  @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
  Long sumAge();                                                                   (7)

  @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
  AggregationResults<SumValue> sumAgeRaw();                                        (8)

  @Aggregation("{ '$project': { '_id' : '$lastname' } }")
  List<String> findAllLastnames();                                                 (9)

  @Aggregation(pipeline = {
		  "{ $group : { _id : '$author', books: { $push: '$title' } } }",
		  "{ $out : 'authors' }"
  })
  void groupAndOutSkippingOutput();                                                (10)
}
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
}
1 聚合管道按 Person 集合中的 lastname 对名字进行分组,并将这些名字作为 PersonAggregate 返回。
2 如果存在 Sort 参数,则 $sort 会附加在已声明的管道阶段之后,以便它仅影响通过所有其他聚合阶段后的最终结果的顺序。因此,Sort 属性会映射到方法返回类型 PersonAggregate,它会将 Sort.by("lastname") 转换为 { $sort : { '_id', 1 } },因为 PersonAggregate.lastname 使用 @Id 进行注释。
3 用给定值替换动态聚合管道中 property?0
4 $skip$limit$sort 可以通过 Pageable 参数传递。与 <2> 中相同,这些运算符会附加到管道定义。接受 Pageable 的方法可以返回 Slice 以简化分页。
5 聚合方法可以返回 Stream 以直接从底层游标使用结果。务必在使用后关闭流以释放服务器端游标,方法是调用 close() 或通过 try-with-resources
6 将返回单个 Document 的聚合结果映射到所需 SumValue 目标类型的实例。
7 仅包含累积结果(如 $sum)的单个文档的聚合可以直接从结果 Document 中提取。为了获得更多控制权,你可以考虑将 AggregationResult 作为方法返回类型,如 <7> 中所示。
8 获取映射到泛型目标包装器类型 SumValueorg.bson.Document 的原始 AggregationResults
9 与 <6> 中类似,可以从多个结果 Document 中直接获取单个值。
10 当返回类型为 void 时,跳过 $out 阶段的输出。

在某些情况下,聚合可能需要其他选项,例如最大运行时间、其他日志注释或临时将数据写入磁盘的权限。使用 @Meta 注释通过 maxExecutionTimeMscommentallowDiskUse 设置这些选项。

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 并检查以下内容

  1. 文档中只有一个条目,返回它。

  2. 两个条目,一个是 _id 值。返回另一个。

  3. 返回可分配给返回类型的第一个值。

  4. 如果上述情况都不适用,则抛出异常。

对于使用 @Aggregation 的存储库方法,不支持 Page 返回类型。但是,可以使用 Pageable 参数将 $skip$limit$sort 添加到管道,并让方法返回 Slice

按示例查询

简介

本章介绍按示例查询,并说明如何使用它。

按示例查询 (QBE) 是一种用户友好的查询技术,具有简单的界面。它允许动态创建查询,并且不需要编写包含字段名称的查询。事实上,按示例查询根本不需要使用特定于存储区的查询语言编写查询。

本章说明按示例查询的核心概念。这些信息是从 Spring Data Commons 模块中提取的。根据您的数据库,字符串匹配支持可能会受到限制。

用法

按示例查询 API 包含四个部分

  • 探查:具有已填充字段的域对象的实际示例。

  • ExampleMatcherExampleMatcher 携带有关如何匹配特定字段的详细信息。它可以在多个示例中重复使用。

  • ExampleExample 包含探查和 ExampleMatcher。它用于创建查询。

  • FetchableFluentQueryFetchableFluentQuery 提供一个流畅的 API,允许进一步自定义从 Example 派生的查询。使用流畅 API 允许您为查询指定排序投影和结果处理。

按示例查询非常适合多种用例

  • 使用一组静态或动态约束查询您的数据存储。

  • 频繁重构域对象,而不用担心破坏现有查询。

  • 独立于底层数据存储 API 工作。

按示例查询也有一些限制

  • 不支持嵌套或分组的属性约束,例如 firstname = ?0 or (firstname = ?1 and lastname = ?2)

  • 字符串匹配的特定于存储的支持。根据你的数据库,字符串匹配可以支持字符串的开始/包含/结束/正则表达式。

  • 其他属性类型的精确匹配。

在开始按示例查询之前,你需要有一个域对象。首先,为你的存储库创建一个接口,如下例所示

示例人员对象
public class Person {

  @Id
  private String id;
  private String firstname;
  private String lastname;
  private Address address;

  // … getters and setters omitted
}

前面的示例显示了一个简单的域对象。你可以使用它来创建一个 Example。默认情况下,具有 null 值的字段将被忽略,并且字符串将使用特定于存储的默认值进行匹配。

将属性包含到按示例查询条件中基于可空性。使用基本类型(intdouble、…)的属性始终包含,除非 ExampleMatcher 忽略属性路径

可以通过使用 of 工厂方法或使用 ExampleMatcher 来构建示例。Example 是不可变的。以下清单显示了一个简单的示例

示例 3. 简单示例
Person person = new Person();                         (1)
person.setFirstname("Dave");                          (2)

Example<Person> example = Example.of(person);         (3)
1 创建一个域对象的新实例。
2 设置要查询的属性。
3 创建 Example

你可以使用存储库运行示例查询。为此,让你的存储库接口扩展 QueryByExampleExecutor<T>。以下清单显示了 QueryByExampleExecutor 接口的摘录

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)
1 创建一个域对象的新实例。
2 设置属性。
3 创建一个 ExampleMatcher 以期望所有值匹配。即使没有进一步的配置,它也可以在此阶段使用。
4 构造一个新的 ExampleMatcher 以忽略 lastname 属性路径。
5 构造一个新的 ExampleMatcher 以忽略 lastname 属性路径并包含空值。
6 构造一个新的 ExampleMatcher 以忽略 lastname 属性路径、包含空值并执行后缀字符串匹配。
7 基于域对象和已配置的 ExampleMatcher 创建一个新的 Example

默认情况下,ExampleMatcher 期望探测器上设置的所有值都匹配。如果你希望获得匹配隐式定义的任何谓词的结果,请使用 ExampleMatcher.matchingAny()

您可以为各个属性(如“firstname”和“lastname”,或对于嵌套属性,为“address.city”)指定行为。您可以使用匹配选项和大小写敏感性对其进行调整,如下例所示

配置匹配器选项
ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", endsWith())
  .withMatcher("lastname", startsWith().ignoreCase());
}

配置匹配器选项的另一种方法是使用 lambda(在 Java 8 中引入)。此方法创建一个回调,要求实现者修改匹配器。您无需返回匹配器,因为配置选项保存在匹配器实例中。以下示例展示了使用 lambda 的匹配器

使用 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 限制查询的属性。firstfirstValueoneoneValueallpagestreamcountexists 定义您获得的结果类型以及当结果数量超过预期数量时查询的行为。

使用 fluent API 获取潜在的多个结果的最后一个结果,按 lastname 排序。
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));
  }
}

滚动

滚动是一种更细粒度的遍历更大结果集块的方法。滚动包括一个稳定排序、一个滚动类型(基于偏移或键集的滚动)和结果限制。您可以使用属性名称定义简单的排序表达式,并使用 TopFirst 关键字 通过查询派生来定义静态结果限制。您可以连接表达式,将多个条件收集到一个表达式中。

滚动查询返回一个 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 的特殊体现,表示滚动操作的开始。

WindowIterator 提供了一个实用程序,通过消除检查下一个 Window 的存在和应用 ScrollPosition 的需要,简化了跨 Window 的滚动。

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(ScrollPosition.offset());

while (users.hasNext()) {
  User u = users.next();
  // consume the user
}

使用偏移量滚动

偏移量滚动使用类似于分页的机制,即偏移量计数器来跳过一定数量的结果,并让数据源仅返回从给定偏移量开始的结果。这种简单的机制避免了将大量结果发送到客户端应用程序。但是,大多数数据库要求在服务器返回结果之前对完整查询结果进行具体化。

示例 6. 将 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)
1 从没有偏移量开始,以包含位置 0 处的元素。

ScollPosition.offset()ScollPosition.offset(0L) 之间存在差异。前者表示滚动操作的开始,不指向任何特定偏移量,而后者标识结果的第一个元素(位于位置 0)。鉴于滚动的独占性质,使用 ScollPosition.offset(0) 会跳过第一个元素并转换为偏移量 1

使用键集筛选滚动

基于偏移量的方法要求大多数数据库在服务器返回结果之前对整个结果进行具体化。因此,虽然客户端只看到请求结果的一部分,但服务器需要构建完整的结果,这会导致额外的负载。

键集筛选通过利用数据库的内置功能来获取结果子集,旨在减少单个查询的计算和 I/O 要求。此方法维护一组键,以便通过将键传递到查询中来恢复滚动,从而有效地修改筛选条件。

Keyset 过滤的核心思想是从使用稳定的排序顺序开始检索结果。一旦想要滚动到下一块,即可获得一个 ScrollPosition,用于重建排序结果中的位置。ScrollPosition 捕获当前 Window 中最后一个实体的键集。要运行查询,重建会重写条件子句以包括所有排序字段和主键,以便数据库可以利用潜在索引来运行查询。数据库只需要从给定的键集位置构建一个更小的结果,而无需完全实现一个大的结果,然后跳过结果直到达到特定偏移量。

Keyset 过滤要求键集属性(用于排序的属性)为非空。此限制适用于存储特定 null 值比较运算符的处理以及针对索引源运行查询的需要。对可空属性进行 Keyset 过滤会导致意外结果。

使用存储库查询方法的 KeysetScrollPosition
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)
1 从最开始开始,不要应用其他过滤。

当数据库包含与排序字段匹配的索引时,Keyset 过滤效果最佳,因此静态排序效果很好。应用 Keyset 过滤的滚动查询要求按排序顺序使用的属性由查询返回,并且这些属性必须映射到返回的实体中。

可以使用接口和 DTO 投影,但是请确保包含已按其排序的所有属性,以避免键集提取失败。

指定 Sort 顺序时,包含与查询相关的排序属性就足够了;如果不希望,则无需确保唯一的查询结果。键集查询机制通过包括主键(或复合主键的任何剩余部分)来修改排序顺序,以确保每个查询结果都是唯一的。

排序结果

MongoDB 存储库允许使用各种方法定义排序顺序。我们来看一下以下示例

排序查询结果
  • 命令式

  • 响应式

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)
}
1 从方法名称派生的静态排序。SortByAgeDesc 导致排序参数为 { age : -1 }
2 使用一个方法参数进行动态排序。Sort.by(DESC, "age") 为排序参数创建 { age : -1 }
3 通过 Query 注解进行静态排序。排序参数按照 sort 属性中所述应用。
4 通过 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);
1 使用名称为 lastname-idx 的索引。
2 @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)
}
1 导致 { 'locale' : 'en_US' } 的静态排序定义。
2 导致 { 'locale' : 'en_US' } 的静态排序定义。
3 根据第二个方法参数的动态排序。允许的类型包括 String(例如 'en_US')、Locacle(例如 Locacle.US)和 Document(例如 new Document("locale", "en_US"))
4 根据第二个方法参数的动态排序。
5 Collation 方法参数应用于查询。
6 如果 Collation 方法参数不为 null,则它将覆盖 @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);
}
1 代替 @Document(collation=…​)
2 代替 @Query(collation=…​)
3 优先使用 @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)
1 为所有存储库操作(包括继承的、非自定义实现的操作)配置读取首选项,这些操作没有查询级别的定义。因此,在这种情况下,读取首选项模式将为 primaryPreferred
2 使用注释 ReadPreference 中定义的读取首选项模式,在本例中为 secondaryPreferred
3 @Query 注释定义了 read preference mode 别名,等同于添加 @ReadPreference 注释。
4 此查询将使用存储库中定义的读取首选项模式。

MongoOperationsQuery API 为 ReadPreference 提供了更精细的控制。