Repository接口查询
嫣夜来 人气:01.查询方法定义详解
repository代理有两种方式从方法名中派生出特定存储查询。
- 通过直接从方法名派生查询。
- 通过使用一个手动定义的查询。
可用的选项取决于实际的商店。然而,必须有一个策略来决定创建什么实际的查询。
2.搜索查询策略
下列策略可用于repository解决基础设施查询。对于 XML 配置,你可以通过 query-lookup-strategy 属性在命名空间配置该策略。对于 Java 配置,你可以使用 Enable${store}Repositories 注解的 queryLookupStrategy属性。某些策略可能不被特定的数据存储所支持。
- CREATE; 试图从查询方法的名称中构建一个特定于存储的查询。一般的做法是,从方法名中删除一组已知的前缀,然后解析方法的其余部分。
- USE_DECLARED_QUERY; 试图找到一个已声明的查询,如果找不到则抛出一个异常。查询可以由某个注解来定义,也可以通过其他方式来声明。请参阅特定存储的文档以找到该存储的可用选项。如果版本库基础设施在启动时没有为该方法找到一个已声明的查询,则会失败。
- CREATE_IF_NOT_FOUND;(默认)结合了 CREATE 和 USE_DECLARED_QUERY。它首先查找一个已声明的查询,如果没有找到已声明的查询,它将创建一个基于方法名的自定义查询。这是默认的查询策略,因此,如果你没有明确地配置任何东西,就会使用这种策略。它允许通过方法名快速定义查询,但也可以根据需要通过引入已声明的查询对这些查询进行自定义调整。
3.查询创建
内置在Spring Data repository基础框架中,查询生成器机制对于在repository的实体上建立约束性查询非常有用。
例1. 从方法名创建查询
interface PersonRepository extends Repository<Person, Long> { List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); // 查询中添加去重关键字 List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); // 查询中关键字忽略大小写 List<Person> findByLastnameIgnoreCase(String lastname); // Enabling ignoring case for all suitable properties List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); // 查询使用使用静态排序规则 List<Person> findByLastnameOrderByFirstnameAsc(String lastname); List<Person> findByLastnameOrderByFirstnameDesc(String lastname); }
解析查询方法名称分为 主语和 谓语。
第一部分(find…By, exists…By)定义了查询的主语,
第二部分构成了谓语。引入句(主语)可以包含进一步的表达。
在find(或其他引入关键词)和By之间的任何文本都被认为是描述性的,除非使用一个限制结果的关键词,如Distinct在要创建的查询上设置一个不同的标志,或Top/First来限制查询结果。
附录中包含了查询方法主语关键词和查询方法谓语关键词的完整列表,包括排序和字母修饰语。然而,第一个By作为分界符,表示实际条件谓词的开始。在一个非常基本的层面上,你可以在实体属性上定义条件,并用And和Or来连接它们。
解析方法的实际结果取决于你为之创建查询的持久性存储。然而,有一些一般性约束需要注意:
- 表达式通常是属性遍历与可以串联的运算符的结合。你可以用AND和OR来组合属性表达式。你还可以得到对运算符的支持,如属性表达式的Between、LessThan、GreaterThan和Like。支持的运算符可能因数据存储的不同而不同,所以请查阅参考文档的适当部分。
- 方法解析器支持为单个属性(例如,findByLastnameIgnoreCase(…))或支持忽略大小写的类型的所有属性(通常是字符串实例–例如,findByLastnameAndFirstnameAllIgnoreCase(…))设置忽略大小写标志。是否支持忽略大小写可能因商店而异,所以请查阅参考文档中的相关章节,了解特定商店的查询方法。
- 你可以通过在引用属性的查询方法中附加一个OrderBy子句,并提供一个排序方向(Asc或Desc)来应用静态排序。要创建一个支持动态排序的查询方法,请参阅 “特殊参数处理”。
4.属性表达式
属性表达式只能引用被管理实体的一个直接属性,如前面的例子所示。在查询创建时,你已经确保解析的属性是被管理的实体类的一个对应属性。然而,你也可以通过遍历嵌套属性来定义约束。考虑一下下面的方法签名。
例2
List<Person> findByAddressZipCode(ZipCode zipCode);
假设一个人有一个带有ZipCode的地址。在这种情况下,该方法创建x.address.zipCode属性遍历。解析算法首先将整个部分(AddressZipCode)解释为属性,并检查实体类中是否有该名称的属性(未加首字母)。如果算法成功,它就使用该属性。如果没有,该算法将原始的骆驼字母部分从右侧分割成头和尾,并试图找到相应的属性–在我们的例子中,是AddressZip和Code。如果该算法找到了具有该头部的属性,它就取其尾部,并从那里继续向下构建树,以刚才描述的方式将尾部分割开来。如果第一次分割不匹配,该算法将分割点移到左边(Address, ZipCode),然后继续。
虽然这在大多数情况下应该是有效的,但该算法有可能选择错误的属性。假设人的类也有一个addressZip属性。该算法将在第一轮分割中已经匹配,选择错误的属性,并且失败(因为addressZip的类型可能没有代码属性)。
为了解决这种模糊性,你可以在你的方法名里面使用_来手动定义遍历点。所以我们的方法名将如下。
例3
List<Person> findByAddress_ZipCode(ZipCode zipCode);
因为我们把下划线字符当作一个保留字符,所以我们强烈建议遵循标准的Java命名惯例(也就是说,不要在属性名中使用下划线,而是使用骆驼大写)。
5.特殊参数处理
为了处理你的查询中的参数,定义方法参数,正如在前面的例子中已经看到的。除此之外,基础设施还能识别某些特定的类型,如 Pageable和 Sort,以动态地将 分页和 排序应用于你的查询。下面的例子演示了这些功能。
例4 在查询方法中使用分页、切割、排序
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, Pageable pageable);
注意:采取Sort和Pageable的API希望将非空值交到方法中。如果你不想应用任何排序或分页,请使用Sort.unsorted()和Pageable.unpaged()方法。
第一个方法让你把org.springframework.data.domain.Pageable实例传递给query方法,以动态地将分页添加到你静态定义的查询中。一个Page知道可用的元素和页面的总数。它是通过基础设施触发一个计数查询来计算总数量。
由于这可能是昂贵的(取决于使用的存储),你可以改成返回一个Slice。一个Slice只知道下一个Slice是否可用,这在走过一个较大的结果集时可能就足够了。
排序选项也是通过Pageable实例处理的。如果你只需要排序,可以在你的方法中添加一个org.springframework.data.domain.Sort参数。正如你所看到的,返回一个List也是可能的。在这种情况下,构建实际的Page实例所需的额外元数据并没有被创建(这反过来意味着不需要发出额外的计数查询)。相反,它限制了查询,只查询给定范围的实体。
要想知道你在整个查询中得到多少页,你必须触发一个额外的计数查询。默认情况下,这个查询是由你实际触发的查询派生出来的。
分页和排序
我们可以通过使用属性名称来定义简单的排序表达式。你可以将表达式连接起来,将多个标准收集到一个表达式中。
例5 定义排序表达式
Sort sort = Sort.by("firstname").ascending() .and(Sort.by("lastname").descending());
对于定义排序表达式的更加类型安全的方式,从定义排序表达式的类型开始,使用方法引用来定义排序的属性。
例6 通过使用类型安全的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,你也可以使用生成的元模型类型来定义排序表达式。
例7 通过使用Querydsl API定义排序表达式
QSort sort = QSort.by(QPerson.firstname.asc()) .and(QSort.by(QPerson.lastname.desc()));
6.限制查询结果
你可以通过使用first或top关键字来限制查询方法的结果,这两个关键字可以互换使用。你可以在top或first后面附加一个可选的数值,以指定要返回的最大结果大小。如果不加数字,就会假定结果大小为1。下面的例子显示了如何限制查询的大小。
例8. 用Top和First限制查询结果的大小
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关键字。另外,对于将结果集限制为一个实例的查询,支持用Optional关键字将结果包入。
如果分页或切片应用于限制性查询的分页(以及可用页数的计算),则会在限制性结果内应用。
通过使用排序参数将结果与动态排序相结合,可以让你表达对 "K "最小元素和 "K "最大元素的查询方法。
7. repository方法返回Collections or Iterables
当查询方法返回多个结果时,可以使用标准的Java Iterable、List和Set来接受返回结果。除此之外,我们还支持返回Spring Data的Streamable,这是Iterable的一个自定义扩展,以及Vavr提供的集合类型。请参考附录中对所有可能的查询方法返回类型的解释。
例9 使用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"));
例10 返回类型为自定义的Streamable包装类
Streamable包装类是为集合类提供的一种特殊的封装类型, 是一种常用的模式,为返回多个元素的查询结果提供API。通常,这些类型是通过调用返回类似集合类型的repository方法,并手动创建包装器类型的实例来使用。你可以避免这个额外的步骤,因为Spring Data允许你使用这些包装器类型作为查询方法的返回类型,如果它们满足以下条件:
- 该类型实现了Streamable。
- 该类型暴露了一个构造函数或一个名为of(…)或valueOf(…)的静态工厂方法,它将Streamable作为参数。
下面的列表显示了一个例子。
class Product { MonetaryAmount getPrice() { … } } @RequiredArgsConstructor(staticName = "of") class Products implements Streamable<Product> { private final Streamable<Product> streamable; public MonetaryAmount getTotal() { return streamable.stream() .map(Priced::getPrice) .reduce(Money.of(0), MonetaryAmount::add); } @Override public Iterator<Product> iterator() { return streamable.iterator(); } } interface ProductRepository implements Repository<Product, Long> { Products findAllByDescriptionContaining(String text); }
一个Product实体类对象暴露了访问产品价格的API。
一个Streamable<Product>的封装类型,可以通过使用Products.of(…)(用Lombok注解创建的工厂方法)构建。使用Streamable<Product>的标准构造函数也可以做到。
封装类型暴露了一个额外的API,计算Streamable<Product>上的新值。
实现Streamable接口并委托给实际结果。
那个包装类型的Products可以直接作为查询方法的返回类型使用。你不需要返回Streamable<Product>并在版本库客户端的查询后手动包装它。
对于Vavr类型集合的支持
Vavr是一个拥抱Java中函数式编程概念的库。它带有一组自定义的集合类型,你可以将其作为查询方法的返回类型,如下表所示。
Vavr 集合类 | 被使用的Vavr 实现类 | 有效的Java源类型 |
---|---|---|
io.vavr.collection.Seq | io.vavr.collection.List | java.util.Iterable |
io.vavr.collection.Set | io.vavr.collection.LinkedHashSet | java.util.Iterable |
io.vavr.collection.Map | io.vavr.collection.LinkedHashMap | java.util.Map |
你可以使用第一列中的类型(或其子类型)作为查询方法的返回类型,并根据实际查询结果的Java类型(第三列),获得第二列中的类型作为实现类型使用。
或者,你可以声明Traversable(相当于Vavr Iterable),然后我们从实际返回值中派生出实现类。也就是说,java.util.List会变成Vavr List或Seq,java.util.Set会变成Vavr LinkedHashSet Set,以此类推。
8.repository方法处理Null
从Spring Data 2.0开始,返回单个聚合实例的存储库CRUD方法使用Java 8的Optional来处理空值。除此之外,Spring Data还支持在查询方法上返回以下封装类型。
- com.google.common.base.Optional
- scala.Option
- io.vavr.control.Option
另外,查询方法可以选择完全不使用包装类型。没有查询结果会通过返回null来表示。Repository 方法返回集合、集合替代物、包装器和流,保证不会返回 null,而是返回相应的空表示。详见 “Repository 查询返回类型”。
注解处理Null
你可以通过使用Spring Framework的nullability注解来表达repository方法的nullability约束。它们提供了一种工具友好的方法,并在运行时选择加入空值检查,如下所示。
- @NonNullApi: 在包的层面上使用,声明参数和返回值的默认行为分别是不接受也不产生空值。
- @NonNull。用于不得为空的参数或返回值(在适用@NonNullApi的参数和返回值上不需要)。
- @Nullable。用在可以为空的参数或返回值上。
Spring注解是用JSR 305注解(一个休眠但广泛使用的JSR)进行元注解的。JSR 305元注释让工具供应商(如IDEA、Eclipse和Kotlin)以通用的方式提供null-safety支持,而无需对Spring注释进行硬编码支持。为了在运行时检查查询方法的无效性约束,你需要通过在package-info.java中使用Spring的@NonNullApi,在包级别上激活无效性,如下例所示。
例11 在 package-info.java中声明Non-nullability
@org.springframework.lang.NonNullApi package com.acme;
一旦非空默认到位,repository的查询方法调用就会在运行时被验证是否有空值约束。如果查询结果违反了定义的约束,就会抛出一个异常。这种情况发生在方法会返回null,但被声明为non-nullable(在版本库所在的包上定义注解的默认值)。如果你想再次选择加入可归零的结果,可以有选择地在个别方法上使用@Nullable。使用本节开头提到的结果封装类型继续按预期工作:一个空的结果被翻译成代表不存在的值。
下面的例子展示了刚才描述的一些技术。
例12. 使用不同的无效性约束
package com.acme; import org.springframework.lang.Nullable; interface UserRepository extends Repository<User, Long> { User getByEmailAddress(EmailAddress emailAddress); @Nullable User findByEmailAddress(@Nullable EmailAddress emailAdress); Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); }
repository驻留在一个包(或子包)中,我们为其定义了非空的行为。
当查询没有产生结果时,抛出一个EmptyResultDataAccessException。
当交给该方法的emailAddress为空时,抛出一个IllegalArgumentException。
当查询没有产生结果时,返回null。也接受null作为emailAddress的值。
当查询没有产生结果时返回Optional.empty()。当交给该方法的emailAddress为null时,抛出一个IllegalArgumentException。
9.查询结果流
你可以通过使用Java 8 Stream <T>作为返回类型来增量地处理查询方法的结果。如下面的例子所示,不把查询结果包裹在Stream中,而是使用数据存储的特定方法来执行流式处理。
例13 用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);
一个流可能包裹了底层数据存储的特定资源,因此,在使用后必须关闭。你可以通过使用close()方法来手动关闭Stream,或者使用Java 7 try-with-resources块来关闭,如下面的例子中所示。
例14 在 try-with-resources 块中处理一个 Stream <T> 的结果
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) { stream.forEach(…); }
目前并非所有的Spring Data模块都支持Stream <T>作为返回类型。
10.异步查询结果
你可以通过使用Spring的异步方法运行能力来异步运行repository查询。这意味着该方法在调用后立即返回,而实际的查询发生在一个已经提交给Spring TaskExecutor的任务中。异步查询与反应式查询不同,不应混合使用。关于响应式支持的更多细节,请参见store-specific文档。下面的例子显示了一些异步查询的情况。
例15. 异步查询结果
@Async Future<User> findByFirstname(String firstname); @Async CompletableFuture<User> findOneByFirstname(String firstname); @Async ListenableFuture<User> findOneByLastname(String lastname);
使用java.util.concurrent.Future作为返回类型。
使用Java 8 java.util.concurrent.CompletableFuture作为返回类型。
使用org.springframework.util.concurrent.ListenableFuture作为返回类型。
加载全部内容