查询方法

本节提供了一些关于 Spring Data JDBC 的实现和使用的具体信息。

大部分你在 Repository 上触发的数据访问操作都会导致数据库查询。定义这样的查询可以通过在 Repository 接口上声明一个方法来实现,如下例所示

带有查询方法的 PersonRepository
interface PersonRepository extends PagingAndSortingRepository<Person, String> {

  List<Person> findByFirstname(String firstname);                                   (1)

  List<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable); (2)

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

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

  Person findByFirstnameAndLastname(String firstname, String lastname);             (5)

  Person findFirstByLastname(String lastname);                                      (6)

  @Query("SELECT * FROM person WHERE lastname = :lastname")
  List<Person> findByLastname(String lastname);                                     (7)
  @Query("SELECT * FROM person WHERE lastname = :lastname")
  Stream<Person> streamByLastname(String lastname);                                     (8)

  @Query("SELECT * FROM person WHERE username = :#{ principal?.username }")
  Person findActiveUser();															(9)
}
1 该方法展示了查询所有具有给定 firstname 的人的查询。该查询是通过解析方法名来派生约束条件的,这些约束条件可以通过 AndOr 进行连接。因此,方法名会生成一个查询表达式,如 SELECT … FROM person WHERE firstname = :firstname
2 使用 Pageable 向数据库传递偏移量和排序参数。
3 返回一个 Slice<Person>。选择 LIMIT+1 行以确定是否有更多数据可供消费。不支持 ResultSetExtractor 自定义。
4 运行一个分页查询,返回 Page<Person>。仅选择给定页面范围内的 数据,并可能执行计数查询以确定总数。不支持 ResultSetExtractor 自定义。
5 根据给定条件查找单个实体。对于非唯一结果,会抛出 IncorrectResultSizeDataAccessException 异常。
6 与 <3> 不同,即使查询产生了更多结果文档,也总是会发出第一个实体。
7 findByLastname 方法展示了查询所有具有给定 lastname 的人的查询。
8 streamByLastname 方法返回一个 Stream,这使得值一旦从数据库返回就可以立即使用。
9 你可以使用 Spring Expression Language 动态解析参数。在示例中,使用 Spring Security 解析当前用户的用户名。

下表显示了查询方法支持的关键字

表 1. 查询方法支持的关键字
关键字 示例 逻辑结果

After

findByBirthdateAfter(Date date)

birthdate > date

GreaterThan

findByAgeGreaterThan(int age)

age > age

GreaterThanEqual

findByAgeGreaterThanEqual(int age)

age >= age

Before

findByBirthdateBefore(Date date)

birthdate < date

LessThan

findByAgeLessThan(int age)

age < age

LessThanEqual

findByAgeLessThanEqual(int age)

age <= age

Between

findByAgeBetween(int from, int to)

age BETWEEN from AND to

NotBetween

findByAgeNotBetween(int from, int to)

age NOT BETWEEN from AND to

In

findByAgeIn(Collection<Integer> ages)

age IN (age1, age2, ageN)

NotIn

findByAgeNotIn(Collection ages)

age NOT IN (age1, age2, ageN)

IsNotNull, NotNull

findByFirstnameNotNull()

firstname IS NOT NULL

IsNull, Null

findByFirstnameNull()

firstname IS NULL

Like, StartingWith, EndingWith

findByFirstnameLike(String name)

firstname LIKE name

NotLike, IsNotLike

findByFirstnameNotLike(String name)

firstname NOT LIKE name

Containing 用于 String

findByFirstnameContaining(String name)

firstname LIKE '%' + name + '%'

NotContaining 用于 String

findByFirstnameNotContaining(String name)

firstname NOT LIKE '%' + name + '%'

(无关键字)

findByFirstname(String name)

firstname = name

Not

findByFirstnameNot(String name)

firstname != name

IsTrue, True

findByActiveIsTrue()

active IS TRUE

IsFalse, False

findByActiveIsFalse()

active IS FALSE

查询派生仅限于可以在 WHERE 子句中使用且无需连接的属性。

查询查找策略

JDBC 模块支持在 @Query 注解中将查询手动定义为字符串,或在属性文件中将其定义为命名查询。

当前,从方法名称派生查询仅限于简单属性,即直接存在于聚合根中的属性。此外,这种方法仅支持 select 查询。

使用 @Query

下例展示了如何使用 @Query 声明一个查询方法

使用 @Query 声明查询方法
interface UserRepository extends CrudRepository<User, Long> {

  @Query("select firstName, lastName from User u where u.emailAddress = :email")
  User findByEmailAddress(@Param("email") String email);
}

默认情况下,将查询结果转换为实体时,使用的 RowMapper 与 Spring Data JDBC 自身生成的查询所使用的相同。你提供的查询必须与 RowMapper 期望的格式匹配。必须提供实体构造函数中使用的所有属性的列。通过 setter、wither 或字段访问设置的属性的列是可选的。结果中没有匹配列的属性将不会被设置。该查询用于填充聚合根、嵌入式实体以及一对一关系,包括原始类型数组(这些数组存储和加载为 SQL 数组类型)。对于 maps、lists、sets 和实体数组,会生成单独的查询。

一对一关系中的属性名称必须以关系名称加上 _ 为前缀。例如,如果上面示例中的 User 有一个带 city 属性的 address,则该 city 的列必须命名为 address_city

请注意,基于字符串的查询不支持分页,也不接受 SortPageRequestLimit 作为查询参数,因为这些查询需要重写。如果你想应用限制,请使用 SQL 表达此意图,并自行将适当的参数绑定到查询中。

查询可以包含 SpEL 表达式。有两种评估方式不同的变体。

第一种变体中,SpEL 表达式以 : 为前缀,并像绑定变量一样使用。这样的 SpEL 表达式将被替换为一个绑定变量,并且该变量将绑定到 SpEL 表达式的结果。

在查询中使用 SpEL
@Query("SELECT * FROM person WHERE id = :#{#person.id}")
Person findWithSpEL(PersonRef person);

这可以用于访问参数的成员,如上面的示例所示。对于更复杂的用例,可以在应用程序上下文中提供一个 EvaluationContextExtension,它反过来可以使任何对象在 SpEL 中可用。

另一种变体可以在查询中的任何位置使用,评估查询的结果将替换查询字符串中的表达式。

在查询中使用 SpEL
@Query("SELECT * FROM #{tableName} WHERE id = :id")
Person findWithSpEL(PersonRef person);

它在第一次执行之前评估一次,并使用添加了 tableNamequalifiedTableName 这两个变量的 StandardEvaluationContext。当表名本身是动态的(因为它们也使用 SpEL 表达式)时,这种用法最为有用。

Spring 完全支持基于 -parameters 编译器标志的 Java 8 参数名发现。在构建中使用此标志作为调试信息的替代方案,可以省略命名参数的 @Param 注解。
Spring Data JDBC 仅支持命名参数。

命名查询

如果在注解中没有提供查询(如前一节所述),Spring Data JDBC 将尝试查找一个命名查询。确定查询名称有两种方式。默认方式是取查询的领域类,即 Repository 的聚合根,取其简单名称,然后附加方法名称,用 . 分隔。或者,@Query 注解有一个 name 属性,可用于指定要查找的查询名称。

命名查询预计在类路径下的属性文件 META-INF/jdbc-named-queries.properties 中提供。

可以通过设置 @EnableJdbcRepositories.namedQueriesLocation 的值来更改该文件的位置。

命名查询的处理方式与注解提供的查询相同。

自定义查询方法

流式结果

当你将 Stream 指定为查询方法的返回类型时,Spring Data JDBC 会在元素可用时立即返回它们。处理大量数据时,这适用于减少延迟和内存需求。

流中包含一个到数据库的打开连接。为了避免内存泄漏,最终需要通过关闭流来关闭该连接。推荐的方法是使用 try-with-resource 子句。这也意味着,一旦到数据库的连接关闭,流将无法获取更多元素,并可能抛出异常。

自定义 RowMapperResultSetExtractor

@Query 注解允许你指定要使用的自定义 RowMapperResultSetExtractor。属性 rowMapperClassresultSetExtractorClass 允许你指定要使用的类,这些类将使用默认构造函数实例化。或者,你可以将 rowMapperClassRefresultSetExtractorClassRef 设置为你 Spring 应用程序上下文中的一个 bean 名称。

如果你不仅想为一个方法使用特定的 RowMapper,而是为所有返回特定类型的自定义查询方法使用它,你可以注册一个 RowMapperMap bean,并为每种方法返回类型注册一个 RowMapper。下例展示了如何注册 DefaultQueryMappingConfiguration

@Bean
QueryMappingConfiguration rowMappers() {
  return new DefaultQueryMappingConfiguration()
    .register(Person.class, new PersonRowMapper())
    .register(Address.class, new AddressRowMapper());
}

根据方法的返回类型,在确定为方法使用哪个 RowMapper 时,遵循以下步骤

  1. 如果类型是简单类型,则不使用 RowMapper

    相反,查询预计返回只有一列的单行,并且会对此值应用到返回类型的转换。

  2. 迭代 QueryMappingConfiguration 中的实体类,直到找到一个作为目标返回类型的超类或接口的类。使用为该类注册的 RowMapper

    迭代按注册顺序进行,因此请确保在注册特定类型之后注册更通用的类型。

如果适用,包装器类型(如集合或 Optional)将被解包。因此,返回类型 Optional<Person> 在前面的过程中使用 Person 类型。

通过 QueryMappingConfiguration@Query(rowMapperClass=…) 或自定义 ResultSetExtractor 使用自定义 RowMapper 会禁用 Entity Callbacks 和 Lifecycle Events,因为结果映射可以在需要时触发自己的事件/回调。

修改查询

你可以使用查询方法上的 @Modifying 注解将查询标记为修改查询,如下例所示

@Modifying
@Query("UPDATE DUMMYENTITY SET name = :name WHERE id = :id")
boolean updateName(@Param("id") Long id, @Param("name") String name);

你可以指定以下返回类型

  • void

  • int (更新记录数)

  • boolean (记录是否已更新)

修改查询直接针对数据库执行。不会调用任何事件或回调。因此,即使带有审计注解的字段在注解查询中未更新,它们也不会被更新。