查询方法
本节提供了一些关于 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)
}
| 1 | 该方法展示了查询所有具有指定 firstname 的人员。
查询语句是通过解析方法名称中的约束条件生成的,这些约束条件可以用 And 和 Or 连接起来。
因此,该方法名会生成一个查询表达式: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 表达式语言(SpEL)动态解析参数。 在示例中,Spring Security 被用于解析当前用户的用户名。 |
下表列出了查询方法所支持的关键字:
| 关键字 | 示例 | 逻辑结果 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
查询派生仅限于可在 WHERE 子句中使用而无需联接(joins)的属性。 |
查询查找策略
JDBC 模块支持在 @Query 注解中将查询手动定义为字符串,或在属性文件中定义为命名查询。
目前,从方法名称派生查询仅限于简单属性,即直接存在于聚合根中的属性。 此外,此方法仅支持 SELECT 查询。
使用@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 数组类型存储和加载的基本类型数组。
对于 Map、List、Set 以及实体数组,会生成单独的查询。
一对一关系的属性,其名称必须以关系名称加上_作为前缀。
例如,如果上面示例中的User有一个名为address的属性,而该city包含一个city属性,那么该address_city对应的列名必须标记为6。
请注意,基于字符串的查询不支持分页,也不接受 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);
它在首次执行前被评估一次,并使用一个 StandardEvaluationContext,其中添加了两个变量 tableName 和 qualifiedTableName。
这种用法在表名本身是动态的情况下最为有用,因为它们也使用了 SpEL 表达式。
Spring 完全支持基于 -parameters 编译器标志的 Java 8 参数名发现功能。
在构建过程中使用此标志作为调试信息的替代方案,您可以省略命名参数上的 @Param 注解。 |
| Spring Data JDBC 仅支持命名参数。 |
命名查询
如果注解中未提供查询语句(如前一节所述),Spring Data JDBC 将尝试查找一个命名查询。
确定查询名称的方式有两种。
默认方式是采用该查询的领域类(即仓库的聚合根)的简单类名,并在其后附加方法名,两者之间用.分隔。
另外,@Query注解还提供了一个name属性,可用于显式指定要查找的查询名称。
命名查询应提供在类路径下的属性文件 META-INF/jdbc-named-queries.properties 中。
该文件的位置可以通过设置 @EnableJdbcRepositories.namedQueriesLocation 的值来更改。
命名查询的处理方式与通过注解提供的查询相同。
流式结果
当你将查询方法的返回类型指定为 Stream 时,Spring Data JDBC 会在元素可用时立即返回它们。 在处理大量数据时,这种方式有助于降低延迟和内存需求。
该流包含一个到数据库的打开连接。
为了避免内存泄漏,必须通过关闭该流来最终关闭此连接。
推荐的做法是使用try-with-resource clause。
这也意味着,一旦数据库连接被关闭,该流将无法获取更多元素,并很可能抛出异常。
自定义RowMapper or 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类型。
通过 RowMapper、QueryMappingConfiguration 使用自定义的 @Query(rowMapperClass=…),或使用自定义的 ResultSetExtractor 会禁用实体回调(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(是否更新了一条记录)
修改型查询直接在数据库上执行。 不会触发任何事件或回调。 因此,如果带审计注解的字段未在注解查询中显式更新,这些字段也不会被更新。