MongoDB-specific Query Methods

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

Example 1. PersonRepository with query methods
Imperative
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`方法显示了对具有给定姓氏的所有人的查询。通过解析该方法名称来获取约束条件,这些条件可以通过 `And`和 `Or`连接起来,从而派生出该查询。因此,该方法名称得到 `{"lastname" : lastname}`的查询表达式。
2 将分页应用于查询。你可以为自己的方法签名装备 `Pageable`参数,然后让方法返回 `Page`实例,Spring Data 就会自动相应地对查询进行分页。
3 表示你可以基于非原始类型的属性进行查询。如果找到多个匹配项,则抛出 IncorrectResultSizeDataAccessException
4 使用 `First`关键字将查询限制为仅返回一个结果。与 <3> 不同,如果找到多个匹配项,此方法不会抛出异常。
5 使用 Java 8 `Stream`在迭代流时读取和转换单个元素。
Reactive
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`的所有人的查询。通过解析该方法名称来获取约束条件,这些条件可以通过 `And`和 `Or`连接起来,从而派生出该查询。因此,该方法名称得到 `{"lastname" : lastname}`的查询表达式。
2 该方法显示了对具有给定 `firstname`的所有人的查询,只要 `firstname`由给定的 `Publisher`发出即可。
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 的参数。

Example 2. Supported keywords for query methods
Keyword Sample Logical result

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)

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" }}

如果属性条件比较一个文档,则文档中字段的顺序和精确相等很重要。

Geo-spatial Queries

正如您在之前的关键词表中看到的,几个关键词在 MongoDB 查询中触发了地理空间操作。Near 关键词允许进一步的修改,如下面几个示例所示。

以下示例展示了如何定义一个 near 查询,用它根据给定点查到所有给定距离内的人员:

Advanced Near queries
  • 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,如下示例所示:

Example 3. Using Distance with Metrics
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)子句。除此之外,实际的距离将根据使用的 Metric 来计算。

(注意,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-based Query Methods and Field Restriction

通过向您的仓库查询方法中添加 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 对象的 firstnamelastnameId 属性。age 属性(java.lang.Integer)未设置,因此其值为 null。

JSON-based Queries with SpEL Expressions

查询字符串和字段定义可以使用 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);
}

表达式可用于调用函数、评估条件和构造值。与 JSON 结合使用的 SpEL 表达式会显现出副作用,因为 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 可能是一种增强查询的有力方法。但是,它们还可能接受广泛的无用参数。确保在将字符串传递到查询之前进行清理,以避免创建漏洞或对查询进行不需要的更改。

表达式支持可以通过查询 SPI 来扩展:EvaluationContextExtensionReactiveEvaluationContextExtension。查询 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

Full-text Search Queries

MongoDB 的全文搜索功能是特定于存储的,因此可以在 `MongoRepository`上找到,而不是在更为通用的 `CrudRepository`上。我们需要一个带有全文索引的文档(请参阅 "`Text Indexes`"以了解如何创建全文索引)。

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

Aggregation Methods

信息库层提供了通过带注释的信息库查询方法与 the aggregation framework交互的方法。与 JSON based queries类似,你可以使用 org.springframework.data.mongodb.repository.Aggregation`注释定义管道。该定义可能包含简单占位符(如 `?0),也可能包含 SpEL expressions ?#{ … }

Example 4. Aggregating Repository Method
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

Running an Example

Query by Example

Introduction

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

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

本小节介绍 Query by Example 的核心概念。该信息从 Spring Data Commons 模块中提取。根据您的数据库,字符串匹配支持可能受限。

Usage

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

  • 探测:带已填充字段的域对象的实际示例。

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

  • ExampleExample 由探测和 ExampleMatcher 组成。它用于创建查询。

  • FetchableFluentQueryFetchableFluentQuery 提供了一个流畅的 API,它允许进一步定制从 Example 派生的查询。使用流畅的 API 可以让你为查询指定排序投影和结果处理。

按示例查询非常适合几种用例:

  • 通过一组静态或动态约束查询你的数据存储。

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

  • 独立于底层数据存储 API 运行。

按示例查询也有一些限制:

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

  • 在字符串匹配方面提供特定于存储的支持。根据你的数据库,字符串匹配可以支持字符串的 starts/contains/ends/regex。

  • 对其他属性类型进行精确匹配。

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

Sample Person object
public class Person {

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

  // … getters and setters omitted
}

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

将属性包含到 Query by Example 标准中基于空值能力。使用基本类型(intdouble、…)的属性始终会包含,除非 <<`ExampleMatcher` 忽略属性路径,query-by-example.matchers>>。

可以通过使用 of 工厂方法或使用 [ExampleMatcher,查询 by示例。匹配器 ]. Example 是不可变的。以下清单显示了一个简单的示例:

Example 5. Simple Example
Person person = new Person();                         1
person.setFirstname("Dave");                          2

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

您可以使用存储库运行示例查询。要做到这一点,让您的存储库接口扩展 QueryByExampleExecutor<T>。以下清单显示了 QueryByExampleExecutor 接口的摘录:

The 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.
}

Example Matchers

示例不仅限于默认设置。您可以使用 ExampleMatcher 指定字符串匹配、null 处理和特定于属性的设置的默认值,如下例所示:

Example 6. Example matcher with customized matching
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 Set properties.
3 创建 ExampleMatcher 来预期匹配所有值。即使在没有进一步配置的情况下,它也可以在此阶段使用。
4 构造一个新的 ExampleMatcher 来忽略 lastname 属性路径。
5 构造一个新的 ExampleMatcher 来忽略 lastname 属性路径并包括空值。
6 构造一个新的 ExampleMatcher 来忽略 lastname 属性路径,以包含空值并执行后缀字符串匹配。
7 基于域对象和已配置的 ExampleMatcher 创建一个新的 Example

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

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

Configuring matcher options
ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", endsWith())
  .withMatcher("lastname", startsWith().ignoreCase());
}

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

Configuring matcher options with lambdas
ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", match -> match.endsWith())
  .withMatcher("firstname", match -> match.startsWith());
}

由 Example 创建的查询使用配置的合并视图。默认匹配设置可以设置在 ExampleMatcher 级别,而各个设置可以应用于特定的属性路径。除非显式定义,否则在 ExampleMatcher 上设置的设置将由属性路径设置继承。属性补丁上的设置优先于默认设置。下表描述了各种 ExampleMatcher 设置的范围:

Table 1. Scope of ExampleMatcher settings
Setting Scope

Null-handling

ExampleMatcher

String matching

ExampleMatcher and property path

Ignoring properties

Property path

Case sensitivity

ExampleMatcher and property path

Value transformation

Property path

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 定义了您获得什么样的结果,以及在可用结果多于预期数量时查询将如何进行。

Use the fluent API to get the last of potentially many results, ordered by lastname.
Optional<Person> match = repository.findBy(example,
    q -> q
        .sortBy(Sort.by("lastname").descending())
        .first()
);

Running an Example

以下示例演示了在使用存储库(在这种情况下为 Person 对象)时如何按示例查询:

Example 7. Query by Example using a repository
public interface PersonRepository extends QueryByExampleExecutor<Person> {

}

public class PersonService {

  @Autowired PersonRepository personRepository;

  public List<Person> findPeople(Person probe) {
    return personRepository.findAll(Example.of(probe));
  }
}

Scrolling

滚动是迭代处理较大结果集区块的一种更细粒度的处理方式。滚动包含稳定的排序、滚动类型(基于偏移或基于键集的滚动)以及结果限制。你可以使用属性名称定义简单的排序表达式,并使用 xref:repositories/query-methods-details.adoc#repositories.limit-query-result[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());

WindowIterator 提供了一个实用工具,可通过消除检查是否存在下一个 Window 和应用 ScrollPosition 的需求来简化跨 Window 的滚动。

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

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

Scrolling using Offset

偏移量滚动类似于分页,它使用偏移量计数器跳过一定数目的结果,并让数据源仅从给定偏移量开始返回结果。此简单机制可避免向客户端应用程序发送较大的结果。但是,在服务器返回结果之前,大多数数据库都需要实现完整的查询结果。

Example 8. Using OffsetScrollPosition with Repository Query Methods
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 上的起始偏移量开始。

Scrolling using Keyset-Filtering

基于偏移量的方法需要大多数数据库在服务器返回结果之前实现全部结果的物化。因此,当客户端只看到请求结果的一部分时,服务器需要构建全部结果,这会导致额外的负载。

键集过滤方法通过利用数据库的内置功能来检索结果子集,旨在降低单个查询的计算和 I/O 需求。此方法维护一组键用于通过查询中传入键来恢复滚动,有效修正筛选条件。

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

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

Using KeysetScrollPosition with Repository Query Methods
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 从一开始开始,不使用其他筛选条件。

当数据库包含与排序字段相匹配的索引时,键集过滤效果最佳,因此静态排序效果很好。应用键集过滤的滚动查询需要将排序顺序中使用的属性通过查询返回,而这些属性必须映射在返回的实体中。

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

在指定 Sort 顺序时,包含与查询相关的排序属性就足够了;如果您不想确保唯一查询结果,就不需要这样做。键集查询机制会通过包括主键(或复合主键的任何剩余部分)来修正排序顺序,确保每个查询结果都是唯一的。

Sorting Results

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

Example 9. Sorting Query Results
Imperative
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 }
Reactive
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);
}

Index Hints

`@Hint`注释允许覆盖MongoDB的默认索引选择,并强制数据库使用指定的索引。

Example 10. Example of index hints
@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 注释。

有关索引创建的更多信息,请参阅 Collection Management 部分。

Collation Support

除了 general Collation Support 之外,存储库还允许为各种操作定义排序规则。

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 方法参数将覆盖 from @Query 的默认 collation(如果非空)。

如果你为存储库查找器方法启用了自动索引创建,则在创建索引时将包含潜在的静态排序定义,如 (1) 和 (2) 所示。

最具体的 Collation 优于其他可能定义的 @@。这意味着方法参数优于查询方法注解,而查询方法注解优于域类型注解。

为了精简整个代码库中整理规则属性的使用,还可以使用`@Collation`注释,它可以用作上述注释的元注释。应用相同的规则和位置,此外,直接使用`@Collation`会取代在`@Query`和其他注释中定义的任何整理规则值。这意味着,如果通过`@Query`和`@Collation`声明了整理规则,则会选择`@Collation`中的那个。

Example 11. Using @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 Instead of @Document(collation=…​).
2 Instead of @Query(collation=…​).
3 支持 @Collation 而不是元数据使用。

Read Preferences

`@ReadPreference`注释允许你配置MongoDB的ReadPreferences

Example 12. Example of read preferences
@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 此查询将使用在存储库中定义的读取优先级模式。

`MongoOperations`和`Query`API为`ReadPreference`提供了更精细的控制。