查询方法
本节提供了一些关于 Spring Data JDBC 实现和使用的具体信息。
你通常在存储库上触发的大多数数据访问操作都会导致对数据库运行查询。定义这样的查询只需在存储库接口上声明一个方法,如下例所示:
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
}
该方法展示了一个查询,用于查找所有具有给定
firstname
的人。该查询是通过解析方法名中的约束条件生成的,这些约束条件可以通过And
和Or
进行连接。因此,方法名最终生成了一个查询表达式:SELECT … FROM person WHERE firstname = :firstname
。使用
Pageable
来传递偏移量和排序参数到数据库。返回一个
Slice<Person>
。查询LIMIT+1
行以确定是否有更多的数据需要处理。不支持ResultSetExtractor
的自定义。运行一个分页查询,返回
Page<Person>
。仅选择给定页面范围内的数据,并可能执行一个计数查询以确定总数量。不支持ResultSetExtractor
的自定义。根据给定条件查找单个实体。如果结果不唯一,则会抛出
IncorrectResultSizeDataAccessException
异常。与 <3> 不同,即使查询返回了更多的结果文档,第一个实体也总是会被返回。
findByLastname
方法展示了一个查询,用于查找所有具有给定lastname
的人。streamByLastname
方法返回一个Stream
,这使得值在从数据库返回后立即可用。你可以使用 Spring 表达式语言来动态解析参数。在示例中,使用了 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 on String | findByFirstnameContaining(String name) | firstname LIKE '%' + name + '%' |
NotContaining on 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
注解中手动定义查询字符串,或在属性文件中定义为命名查询。
从方法名称派生查询目前仅限于简单属性,即直接存在于聚合根中的属性。此外,这种方法仅支持选择查询。
使用 @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);
}
为了将查询结果转换为实体,默认情况下使用的是与 Spring Data JDBC 自身生成的查询相同的 RowMapper
。你提供的查询必须匹配 RowMapper
所期望的格式。必须提供实体构造函数中使用的所有属性的列。通过 setter、wither 或字段访问设置的属性的列是可选的。结果中没有匹配列的属性将不会被设置。该查询用于填充聚合根、嵌入实体和一对一关系,包括作为 SQL 数组类型存储和加载的原始类型数组。对于实体类型的映射、列表、集合和数组,会生成单独的查询。
属性一对一关系必须在其名称前加上关系名称和下划线 _
。例如,如果上面的示例中的 User
有一个 address
属性,且该 address
包含 city
属性,那么 city
的列必须标记为 address_city
。
请注意,基于字符串的查询不支持分页,也不接受 Sort
、PageRequest
和 Limit
作为查询参数,因为这些查询需要重新编写。如果您希望应用限制,请使用 SQL 表达此意图,并自行将适当的参数绑定到查询中。
查询可能包含 SpEL 表达式。有两种变体,它们的计算方式不同。
在第一个变体中,SpEL 表达式以 :
为前缀,并像绑定变量一样使用。这样的 SpEL 表达式将被替换为一个绑定变量,并且该变量将绑定到 SpEL 表达式的结果上。
@Query("SELECT * FROM person WHERE id = :#{#person.id}")
Person findWithSpEL(PersonRef person);
这可以用于访问参数的成员,如上例所示。对于更复杂的用例,可以在应用程序上下文中提供 EvaluationContextExtension
,从而使得任何对象都可以在 SpEL 中使用。
另一个变体可以在查询中的任何地方使用,评估查询的结果将替换查询字符串中的表达式。
@Query("SELECT * FROM #{tableName} WHERE id = :id")
Person findWithSpEL(PersonRef person);
它在首次执行之前被评估一次,并使用一个带有两个变量 tableName
和 qualifiedTableName
的 StandardEvaluationContext
。这种用法在表名本身是动态的情况下最为有用,因为它们也使用 SpEL 表达式。
Spring 完全支持基于 Java 8 的 -parameters
编译器标志的参数名发现机制。通过在构建中使用此标志来替代调试信息,你可以为命名参数省略 @Param
注解。
Spring Data JDBC 仅支持命名参数。
命名查询
如果在上一节中描述的注解中没有给出查询,Spring Data JDBC 将尝试定位一个命名查询。确定查询名称的方式有两种。默认的方式是获取查询的领域类,即仓库的聚合根,取其简单名称,并在其后追加方法名称,两者之间以 .
分隔。另一种方式是使用 @Query
注解的 name
属性,该属性可用于指定要查找的查询的名称。
命名查询应在类路径下的属性文件 META-INF/jdbc-named-queries.properties
中提供。
该文件的位置可以通过为 @EnableJdbcRepositories.namedQueriesLocation
设置一个值来更改。
命名查询的处理方式与通过注解提供的查询相同。
自定义查询方法
流式处理结果
当你指定 Stream
作为查询方法的返回类型时,Spring Data JDBC 会在元素可用时立即返回它们。在处理大量数据时,这有助于降低延迟和内存需求。
流中包含一个与数据库的开放连接。为了避免内存泄漏,最终需要通过关闭流来关闭该连接。推荐的做法是使用 try-with-resource
语句。这也意味着,一旦与数据库的连接关闭,流将无法获取更多元素,并且很可能会抛出异常。
自定义 RowMapper
或 ResultSetExtractor
@Query
注解允许你指定自定义的 RowMapper
或 ResultSetExtractor
来使用。属性 rowMapperClass
和 resultSetExtractorClass
允许你指定要使用的类,这些类将使用默认构造函数进行实例化。或者,你也可以将 rowMapperClassRef
或 resultSetExtractorClassRef
设置为 Spring 应用上下文中的 bean 名称。
如果你想使用某个 RowMapper
不仅限于单个方法,而是用于所有返回特定类型的自定义查询方法,你可以注册一个 RowMapperMap
bean,并为每个方法返回类型注册一个 RowMapper
。以下示例展示了如何注册 DefaultQueryMappingConfiguration
:
@Bean
QueryMappingConfiguration rowMappers() {
return new DefaultQueryMappingConfiguration()
.register(Person.class, new PersonRowMapper())
.register(Address.class, new AddressRowMapper());
}
在确定方法使用哪个 RowMapper
时,会根据方法的返回类型遵循以下步骤:
-
如果类型是简单类型,则不使用
RowMapper
。相反,查询预期会返回包含单列的单行,并将该值转换为返回类型。
-
遍历
QueryMappingConfiguration
中的实体类,直到找到一个是返回类型的超类或接口的类。使用为该类注册的RowMapper
。遍历按照注册顺序进行,因此请确保在注册特定类型之后注册更通用的类型。
如果适用,诸如集合或 Optional
的包装类型会被解包。因此,返回类型为 Optional<Person>
时,在前述过程中会使用 Person
类型。
通过 QueryMappingConfiguration
、@Query(rowMapperClass=…)
或自定义的 ResultSetExtractor
使用自定义的 RowMapper
会禁用实体回调和生命周期事件,因为结果映射可以根据需要发出自己的事件/回调。
修改查询
你可以通过在查询方法上使用 @Modifying
来标记查询为修改查询,如下例所示:
@Modifying
@Query("UPDATE DUMMYENTITY SET name = :name WHERE id = :id")
boolean updateName(@Param("id") Long id, @Param("name") String name);
你可以指定以下返回类型:
-
void
-
int
(更新的记录数) -
boolean
(是否更新了记录)
修改查询会直接针对数据库执行。不会调用任何事件或回调。因此,如果带有审计注解的字段在注解查询中没有被更新,那么这些字段也不会被更新。