Spring Data的Repositories----定义查询方法


存储库代理有两种方法从方法名称派生特定于存储的查询:

  • 通过直接从方法名称派生查询。
  • 通过使用手动定义的查询。

可用的选项取决于实际的存储。但是,必须有一个策略来决定创建什么实际查询。下一节介绍了可用的选项。

一、 查询查找策略

以下策略可用于存储库基础结构来解析查询。使用XML配置,可以通过query-lookup-strategy属性在命名空间中配置策略。对于Java配置,可以使用EnableMongoRepositories/EnableRedisRepositories/EnableElasticsearchRepositories注解的queryLookupStrategy属性。某些策略可能不支持特定的数据存储。

  • CREATE尝试根据查询方法名称构造特定于存储的查询。一般的方法是从方法名称中删除一组已知的前缀,然后解析该方法的其余部分。你可以在“21.2 创建查询”中阅读更多关于查询构造的信息。
  • USE_DECLARED_QUERY尝试查找已声明的查询,如果找不到则抛出异常。查询可以由某个地方的注解定义,也可以通过其他方式声明。请参阅特定存储的文档以查找该存储的可用选项。如果存储库基础结构在引导时找不到该方法的已声明查询,则它将失败。
  • CREATE_IF_NOT_FOUND(默认值)结合了CREATE和USE_DECLARED_QUERY。它首先查找一个已声明的查询,如果找不到已声明查询,则创建一个基于自定义方法名称的查询。这是默认的查找策略,因此,如果你没有明确配置任何内容,就会使用它。它允许通过方法名称快速定义查询,但也可以通过根据需要引入声明的查询来自定义调整这些查询。

二、创建查询

Spring Data存储库基础结构中内置的查询生成器机制对于在存储库的实体上构建约束查询非常有用。
以下示例展示了如何创建多个查询:
从方法名称创建查询

interface PersonRepository extends Repository<Person, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

解析查询方法名称分为主题(subject)和谓词(predicate)。第一部分(find…By, exists…By)定义了查询的主题,第二部分形成了谓词。引入子句(subject)可以包含其他表达式。find(或其他引入关键字)和By之间的任何文本都被认为是描述性的,除非使用结果限制关键字之一,如Distinct在要创建的查询上设置不同的标志,或使用Top/First限制查询结果(参见21.7.3 限制查询结果)。
章节十九包含“19.1 查询方法主题关键字的完整列表”和“19.2查询方法谓词关键字,包括排序和大小写修饰符”。这当中,第一个By充当分隔符,指示实际标准谓词的开始。作为基础的功能,你可以定义实体属性上的条件,并使用And和Or将它们连接起来。
解析该方法的实际结果取决于你所创建查询的持久性存储。然而,有一些一般的事情需要注意:

  • 表达式通常是与可以连接的运算符组合的属性遍历。可以将属性表达式与AND和OR组合使用。你还可以获得对属性表达式的运算符(如Between、LessThan、GreaterThan和Like)的支持。支持的运算符可能因数据存储而异,因此请参阅参考文档的相应部分。
  • 方法解析器支持“为单个属性(例如findByLastnameIgnoreCase(…))设置IgnoreCase flag ”或“为支持忽略大小写的类型的所有属性(通常为String实例 — 例如,findByLastnameAndFirstnameAllIgnoreCase(…))“。是否支持忽略大小写可能因存储而异,因此请参阅参考文档中的相关章节以了解特定存储的查询方法。
  • 可以通过将OrderBy子句附加到引用属性的查询方法并提供排序方向(Asc或Desc)来应用静态排序。要创建支持动态排序的查询方法,请参阅“21.7 分页, 迭代大结果集, 排序 & 限制”。

三、属性表达式

属性表达式只能引用托管(managed)实体的直接属性,如前面的示例所示。在创建查询时,你已经确保解析的属性是托管域类的属性。但是,你也可以通过遍历嵌套属性来定义约束。参见以下方法签名:

List<Person> findByAddressZipCode(ZipCode zipCode);

假设一个Person有一个带有ZipCode的Address。在这种情况下,该方法创建x.address.zipCode属性遍历。解析算法首先将整个部分(AddressZipCode)解释为属性,并在域类中检查具有该名称(未大写)的属性。如果算法成功,它将使用该属性。如果没有,算法会将camel-case部分的源从右侧拆分为头和尾,并试图找到相应的属性——在我们的示例中是AddressZip和Code。如果算法找到了一个具有该头部的属性,它会获取尾部并继续从那里构建树,以刚才描述的方式将尾部向上拆分。如果第一个分割不匹配,则算法将分割点向左移动(Address,ZipCode)并继续。
尽管这应该适用于大多数情况,但算法可能会选择错误的属性。假设Person类也有一个addressZip 属性。该算法在第一轮拆分中已经匹配,选择了错误的属性,然后失败(因为addressZip的类型可能没有code属性)。
为了解决这种歧义,你可以在方法名称中使用“_”来手动定义遍历点。因此,我们的方法名称如下:

List<Person> findByAddress_ZipCode(ZipCode zipCode);

由于我们将下划线字符视为保留字符,因此强烈建议遵循标准的Java命名约定(即,在属性名称中不使用下划线,而是使用驼峰大小写)。

四、返回Collections或Iterables的存储库方法

返回多个结果的查询方法可以使用标准的Java Iterable、List和Set。除此之外,框架还支持返回Spring Data的Streamable(Iterable的自定义扩展)以及Vavr提供的集合类型。请参阅章节二十,其中解释了所有可能的查询方法返回类型。

4.1 使用Streamable作为查询方法返回类型

你可以使用Streamable来替代Iterable或任何集合类型。它提供了访问非并行流(Iterable中缺少)的方便方法,并能够直接在元素上“….filter(…)”和“….map(…))”,并将Streamable连接(concatenate)到其他元素:
使用Streamable组合查询方法结果

interface PersonRepository extends Repository<Person, Long> {
  Streamable<Person> findByFirstnameContaining(String firstname);
  Streamable<Person> findByLastnameContaining(String lastname);
}

Streamable<Person> result = repository.findByFirstnameContaining("av")
  .and(repository.findByLastnameContaining("ea"));

4.2 返回自定义流包装类型

为集合提供专用包装器类型是为返回多个元素的查询结果提供API的常用模式。通常,这些类型是通过调用存储库方法来使用的,该方法返回类似集合的类型并手动创建包装器类型的实例。你可以避免这个额外的步骤,因为Spring Data允许你使用这些包装类型作为查询方法返回类型,只需满足以下条件:

  1. 类型实现Streamable。
  2. 该类型公开一个构造函数或一个名为of(…)或valueOf(…)的静态工厂方法,该方法将Streamable作为参数。

以下列表展示了一个示例:

class Product {                                  --------1       
  MonetaryAmount getPrice() {}
}

@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> {  --------2        

  private final Streamable<Product> streamable;

  public MonetaryAmount getTotal() {             --------3       
    return streamable.stream()
      .map(Priced::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }


  @Override
  public Iterator<Product> iterator() {          --------4       
    return streamable.iterator();
  }
}

interface ProductRepository implements Repository<Product, Long> {
  Products findAllByDescriptionContaining(String text); --------5
}

1. 公开API以访问产品价格的产品实体。
2. Streamable<Product>的包装类型,可以使用Products.of()(使用Lombok注解创建的工厂方法)来构建。采用Streamable<Product>的标准构造函数也可以。
3. 包装器类型公开了一个额外的API,用于计算Streamable<Product>上的新值。
4. 实现Streamable接口并委托给实际结果。
5. 该包装类型Products可以直接用作查询方法返回类型。你不需要返回Streamable<Product>也不必在存储库客户端中查询后手动包装它。

4.3 Vavr集合的支持

Vavr是一个包含Java函数式编程概念的库。它附带了一组自定义的集合类型,可以用作查询方法返回类型,如下表所示:

Vavr集合类型使用的Vavr实现类型有效的Java源类型
io.vavr.collection.Seqio.vavr.collection.Listjava.util.Iterable
io.vavr.collection.Setio.vavr.collection.LinkedHashSetjava.util.Iterable
io.vavr.collection.Mapio.vavr.collection.LinkedHashMapjava.util.Map

根据实际查询结果(第三列)的Java类型,你可以使用第一列中的类型(或其子类型)作为查询方法返回类型,并将第二列中的这些类型用作实现类型。或者,你可以声明Traversable(等效于Vavr Iterable),然后我们从实际返回值派生实现类。也就是说,一个java.util.List变成了一个Vavr List或Seq,一个java.util.Set变为Vavr LinkedHashSet Set,等等。

五、流查询结果

你可以使用Java 8 Stream< T>作为返回类型来增量处理查询方法的结果。使用特定于数据存储的方法来执行流传输,而不是将查询结果包装在流中,如以下示例所示:
使用Java 8 Stream<T>流式传输查询结果

@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);

Stream可能会包装底层数据存储特定的资源,因此在使用后必须关闭。你可以使用close()方法手动关闭Stream,也可以使用Java 7 try-with-resources块,如以下示例所示:
在try-with-resources块中使用Stream< T>的结果

try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach();
}

并非所有Spring Data模块当前都支持Stream<T>作为返回类型。

六、异步查询结果

通过使用Spring的异步方法运行功能,你可以异步运行存储库查询。这意味着该方法在调用时立即返回,而实际查询发生在已提交给Spring TaskExecutor的任务中。异步查询不同于响应式(reactive)查询,不应该混合使用。有关响应式支持的更多细节,请参阅特定存储的文档。下面的例子展示了一些异步查询:

@Async
Future<User> findByFirstname(String firstname);               --------1

@Async
CompletableFuture<User> findOneByFirstname(String firstname); --------2

1. 使用java.util.concurrent.Future作为返回类型。
2. 使用java.util.concurrent.CompletableFuture作为返回类型。

七、分页, 迭代大结果集, 排序 & 限制

若要处理查询中的参数,请定义前面示例中已经看到的方法参数。除此之外,基础结构还可以识别某些特定类型,如Pageable、Sort和Limit,以动态地将分页、排序和限制应用于查询。以下示例演示了这些功能:
在查询方法中使用Pageable、Slice、Sort和Limit

Page<User> findByLastname(String lastname, Pageable pageable);

Slice<User> findByLastname(String lastname, Pageable pageable);

List<User> findByLastname(String lastname, Sort sort);

List<User> findByLastname(String lastname, Sort sort, Limit limit);

List<User> findByLastname(String lastname, Pageable pageable);

采用Sort、Pageable和Limit的API期望将非空值传递到方法中。如果不想应用任何排序或分页,请使用Sort.unsorted()、Pageable.unpaged()和Limit.unlimited()。
第一种方法允许你将“org.springframework.data.domain.Pageable”实例传递给查询方法,以动态地将分页添加到静态定义的查询中。Page知道可用元素和页面的总数。它是通过基础设施触发计数查询来计算总数来实现的。由于这样成本可能很高(取决于使用的存储),你可以返回Slice。Slice只知道下一个Slice是否可用,这在遍历较大的结果集时可能就足够了。
排序选项也通过Pageable实例进行处理。如果只需要排序,请在方法中添加一个“org.springframework.data.domain.Sort”参数。正如你所看到的,返回List也是可能的。在这种情况下,不会创建构建实际Page实例所需的额外元数据(这反过来意味着没有触发本来必要的额外计数查询)。相反,它限制查询只查找给定范围的实体。
要了解整个查询的页数,必须触发一个额外的计数查询。默认情况下,此查询派生自实际触发的查询。
特殊参数在查询方法中只能使用一次。
上面描述的一些特殊参数是互斥的。参见以下无效参数组合列表。

参数组合例子原因
Pageable和SortfindBy…​(Pageable page, Sort sort)Pageable已经定义了Sort
Pageable和LimitfindBy…​(Pageable page, Limit limit)Pageable已经定义了limit

用于限制结果的Top关键字可以与Pageable一起使用,Top定义了结果总数的最大值,而Pageable 参数可能减少这个数字。

7.1 哪种方法是合适的?

Spring Data抽象提供的值可能最好通过下表中列出的查询方法返回类型来显示。该表显示了可以从查询方法返回的类型
表1:消费大量查询结果

方法获取的数据量查询结构约束
List< T>所有的结果单个查询查询结果可能耗尽所有内存。获取所有数据可能非常耗时。
Streamable< T>所有的结果单个查询查询结果可能耗尽所有内存。获取所有数据可能非常耗时。
Stream< T>分块(一个接一个或分批)取决于流的消费。使用典型游标的单个查询。流在使用后必须关闭,以避免资源泄漏。
Flux< T>根据Flux消费进行分块(逐个或分批)。使用典型游标的单个查询。存储模块必须提供响应式(reactive)基础设施。
Slice< T>在Pageable.getOffset()处的Pageable.getPageSize() + 1一个到多个使用了limit的从Pageable.getOffset()开始获取数据的查询。切片(Slice)只能导航到下一个切片。Slice提供了是否有更多数据要获取的详细信息。Window提供了是否有更多数据要获取的详细信息。当偏移量太大时,基于偏移量的查询会变得效率低下,因为数据库仍然必须物化(materialize )完整的结果。
Page< T>Pageable.getOffset()处的Pageable.getPageSize()从Pageable.getOffset()开始的使用了limit的一到多个查询。此外,可能需要COUNT(…)查询来确定元素的总数。通常情况下,需要COUNT(…) 的查询是成本较高的。当偏移量太大时,基于偏移量的查询会变得效率低下,因为数据库仍然需要物化(materialize )完整的结果。

7.2 分页和排序

你可以使用属性名定义简单的排序表达式。你也可以连接表达式,将多个条件收集到一个表达式中。
定义排序表达式

Sort sort = Sort.by("firstname").ascending()
  .and(Sort.by("lastname").descending());

若要以更类型安全的方式定义排序表达式,请从定义排序表达式的类型开始,并使用方法引用来定义要对其进行排序的属性。
使用类型安全的API定义排序表达式

TypedSort<Person> person = Sort.sort(Person.class);

Sort sort = person.by(Person::getFirstname).ascending()
  .and(person.by(Person::getLastname).descending());

TypedSort.by(…) 通常通过CGlib来使用运行时代理,这可能会在使用Graal VM Native等工具时干扰本机映像编译。
如果你的存储实现支持Querydsl,你还可以使用生成的元模型类型来定义排序表达式:
使用Querydsl API定义排序表达式

QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));

7.3 限制查询结果

除了分页之外,还可以使用专用的Limit参数来限制结果大小。还可以使用First或Top关键字来限制查询方法的结果,这两个关键字可以互换使用,但不能与Limit参数混合使用。你可以在Top或First后面附加一个可选的数值,以指定要返回的最大结果大小。如果忽略该数字,则假定结果大小为1。以下示例展示了如何限制查询大小:
使用Top和First限制查询的结果大小

List<User> findByLastname(Limit limit);

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

对于支持distinct查询的数据存储,限制表达式还支持Distinct关键字。此外,对于将结果集限制为一个实例的查询,支持使用Optional关键字将结果包装到其中。
如果将分页或切片应用于限制查询分页(以及可用页面数的计算),则它将在有限的结果中应用。
通过使用Sort参数限制结果并结合动态排序,可以表达最小“K”个和最大“K”个元素的查询方法。

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值