原文:https://www.docs4dev.com/docs/zh/spring-data-jdbc/1.0.5.RELEASE/reference/all.html
Spring Data JDBC提供基于JDBC的存储库抽象。
Bugtracker: https://jira.spring.io/browse/DATAJDBC
本节介绍每个版本的重大更改。
CrudRepository
的基本支持。
@Query
支持。
MyBatis支持。
Id代。
活动支持。
审计。
CustomConversions
。
由于各个Spring Data模块的初始日期不同,因此大多数模块都带有不同的主要版本号和次要版本号。找到兼容版本的最简单方法是依赖我们随定义的兼容版本提供的Spring Data Release Train BOM。在Maven项目中,您将在POM的 <dependencyManagement />
部分中声明此依赖项,如下所示:
示例1.使用Spring Data版本系列BOM
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-releasetrain</artifactId>
<version>Lovelace-SR5</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
目前的发布列车版本是 Lovelace-SR5
。列车名称按字母顺序上升,当前可用的列车列于 here 。版本名称遵循以下模式: ${name}-${release}
,其中release可以是以下之一:
BUILD-SNAPSHOT
:当前快照
M1
, M2
,等等:里程碑
RC1
, RC2
,等等:发布候选人
RELEASE
:GA发布
SR1
, SR2
等:服务版本
可以在我们的 Spring Data examples repository 中找到使用BOM的工作示例。有了这些,您可以声明要在 <dependencies />
块中没有版本的情况下使用的Spring Data模块,如下所示:
示例2.声明对Spring Data模块的依赖关系
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependencies>
Spring Boot为您选择最新版本的Spring Data模块。如果您仍想升级到较新版本,请将属性 spring-data-releasetrain.version
配置为您要使用的 train name and iteration 。
当前版本的Spring Data模块需要版本5.1.5.RELEASE或更高版本的Spring Framework。这些模块也可以使用该次要版本的旧版本。但是,强烈建议使用该代中的最新版本。
Spring Data存储库抽象的目标是显着减少为各种持久性存储实现数据访问层所需的样板代码量。
Spring Data存储库文档和您的模块
本章介绍Spring Data存储库的核心概念和接口。本章中的信息来自Spring Data Commons模块。它使用Java Persistence API(JPA)模块的配置和代码示例。您应该将XML名称空间声明和要扩展的类型调整为您使用的特定模块的等效项。 “ Namespace reference ”涵盖XML配置,支持存储库API的所有Spring Data模块都支持XML配置。 “ Repository query keywords ”涵盖了存储库抽象支持的查询方法关键字。有关模块特定功能的详细信息,请参阅本文档该模块的章节。
Spring Data存储库抽象中的中央接口是 Repository
。它将域类以及域类的ID类型作为类型参数进行管理。此接口主要用作标记接口,用于捕获要使用的类型,并帮助您发现扩展此接口的接口。 CrudRepository
为正在管理的实体类提供了复杂的CRUD功能。
例3. CrudRepository
接口
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity); (1)
Optional<T> findById(ID primaryKey); (2)
Iterable<T> findAll(); (3)
long count(); (4)
void delete(T entity); (5)
boolean existsById(ID primaryKey); (6)
// … more functionality omitted.
}
1 | 保存给定的实体。 |
---|---|
2 | 返回由给定ID标识的实体。 |
3 | 返回所有实体。 |
4 | 返回实体数量。 |
5 | 删除给定的实体。 |
6 | 表示是否存在具有给定ID的实体。 |
我们还提供特定于持久性技术的抽象,例如
JpaRepository
或MongoRepository
。除了相当通用的与持久性技术无关的接口(如CrudRepository
)之外,这些接口还扩展了CrudRepository
并公开了底层持久性技术的功能。
在 CrudRepository
之上,有一个 PagingAndSortingRepository
抽象,它添加了额外的方法来简化对实体的分页访问:
示例4. PagingAndSortingRepository
接口
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
要以页面大小20访问 User
的第二页,您可以执行以下操作:
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));
除了查询方法,可以使用计数和删除查询的查询派生。以下列表显示派生计数查询的接口定义:
示例5.派生计数查询
interface UserRepository extends CrudRepository<User, Long> {
long countByLastname(String lastname);
}
以下列表显示了派生删除查询的接口定义:
示例6.派生的删除查询
interface UserRepository extends CrudRepository<User, Long> {
long deleteByLastname(String lastname);
List<User> removeByLastname(String lastname);
}
标准CRUD功能存储库通常对基础数据存储区进行查询。使用Spring Data,声明这些查询将分为四个步骤:
interface PersonRepository extends Repository<Person, Long> { … }
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByLastname(String lastname);
}
设置Spring以为这些接口创建代理实例,使用 JavaConfig 或 XML configuration 。
要使用Java配置,请创建类似于以下内容的类:
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories
class Config { … }
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<jpa:repositories base-package="com.acme.repositories"/>
</beans>
在此示例中使用JPA名称空间。如果对任何其他 Store 使用存储库抽象,则需要将其更改为 Store 模块的相应名称空间声明。换句话说,您应该交换 jpa
,例如, mongodb
。
另请注意,JavaConfig变体未显式配置包,因为默认情况下使用带注释的类的包。要自定义要扫描的包,请使用特定于数据存储库的 @Enable${store}Repositories
-annotation的 basePackage…
属性之一。
class SomeClient {
private final PersonRepository repository;
SomeClient(PersonRepository repository) {
this.repository = repository;
}
void doSomething() {
List<Person> persons = repository.findByLastname("Matthews");
}
}
以下部分详细说明了每个步骤:
首先,定义特定于域类的存储库接口。接口必须扩展 Repository
并键入域类和ID类型。如果要公开该域类型的CRUD方法,请扩展 CrudRepository
而不是 Repository
。
通常,存储库接口扩展 Repository
, CrudRepository
或 PagingAndSortingRepository
。或者,如果您不想扩展Spring Data接口,还可以使用 @RepositoryDefinition
注释存储库接口。扩展 CrudRepository
公开了一整套操作实体的方法。如果您希望对所公开的方法有选择性,请将要从 CrudRepository
公开的方法复制到域存储库中。
这样做可以让您在提供的Spring Data Repositories功能之上定义自己的抽象。
以下示例显示如何有选择地公开CRUD方法(在本例中为 findById
和 save
):
示例7.有选择地暴露CRUD方法
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {
Optional<T> findById(ID id);
<S extends T> S save(S entity);
}
interface UserRepository extends MyBaseRepository<User, Long> {
User findByEmailAddress(EmailAddress emailAddress);
}
在前面的示例中,您为所有域存储库定义了一个公共基本接口,并公开了 findById(…)
以及 save(…)
。这些方法被路由到Spring Data提供的所选 Store 的基本存储库实现中(例如,如果您使用JPA,实现是 SimpleJpaRepository
),因为它们匹配 CrudRepository
中的方法签名。因此 UserRepository
现在可以保存用户,按ID查找单个用户,并触发查询以通过电子邮件地址查找 Users
。
中间存储库接口使用
@NoRepositoryBean
进行批注。确保将该注释添加到Spring Data不应在运行时创建实例的所有存储库接口。
在应用程序中使用唯一的Spring Data模块会使事情变得简单,因为定义范围内的所有存储库接口都绑定到Spring Data模块。有时,应用程序需要使用多个Spring Data模块。在这种情况下,存储库定义必须区分持久性技术。当它在类路径上检测到多个存储库工厂时,Spring Data进入严格的存储库配置模式。严格配置使用存储库或域类的详细信息来确定存储库定义的Spring Data模块绑定:
如果存储库定义 extends the module-specific repository ,那么它是特定Spring Data模块的有效候选者。
如果域类是 annotated with the module-specific type annotation ,那么它是特定Spring Data模块的有效候选者。 Spring Data模块接受第三方注释(例如JPA的 @Entity
)或提供自己的注释(例如Spring数据MongoDB和Spring Data Elasticsearch的 @Document
)。
以下示例显示了使用特定于模块的接口的存储库(在本例中为JPA):
示例8.使用模块特定接口的存储库定义
interface MyRepository extends JpaRepository<User, Long> { }
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { … }
interface UserRepository extends MyBaseRepository<User, Long> { … }
MyRepository
和 UserRepository
在其类型层次结构中扩展 JpaRepository
。它们是Spring Data JPA的有效候选者模块。
以下示例显示了使用通用接口的存储库:
示例9.使用通用接口的存储库定义
interface AmbiguousRepository extends Repository<User, Long> { … }
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { … }
interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { … }
AmbiguousRepository
和 AmbiguousUserRepository
在其类型层次结构中仅扩展 Repository
和 CrudRepository
。虽然这在使用独特的Spring Data模块时非常好,但是多个模块无法区分这些存储库应该绑定到哪个特定的Spring Data。
以下示例显示了使用带注释的域类的存储库:
示例10.使用带注释的域类的存储库定义
interface PersonRepository extends Repository<Person, Long> { … }
@Entity
class Person { … }
interface UserRepository extends Repository<User, Long> { … }
@Document
class User { … }
PersonRepository
引用 Person
,它使用JPA @Entity
注释进行注释,因此该存储库显然属于Spring Data JPA。 UserRepository
引用 User
,它使用Spring Data MongoDB的 @Document
注释进行注释。
以下错误示例显示了使用具有混合注释的域类的存储库:
示例11.使用具有混合注释的域类的存储库定义
interface JpaPersonRepository extends Repository<Person, Long> { … }
interface MongoDBPersonRepository extends Repository<Person, Long> { … }
@Entity
@Document
class Person { … }
此示例显示了使用JPA和Spring Data MongoDB注释的域类。它定义了两个存储库 JpaPersonRepository
和 MongoDBPersonRepository
。一个用于JPA,另一个用于MongoDB用法。 Spring Data不再能够将存储库分开,从而导致未定义的行为。
Repository type details 和 distinguishing domain class annotations 用于严格的存储库配置,以识别特定Spring Data模块的存储库候选。在同一域类型上使用多个持久性技术特定的注释是可能的,并允许跨多种持久性技术重用域类型。但是,Spring Data不再能够确定用于绑定存储库的唯一模块。
区分存储库的最后一种方法是通过确定存储库基础包的范围。基础包定义了扫描存储库接口定义的起点,这意味着将存储库定义放在相应的包中。默认情况下,注释驱动的配置使用配置类的包。 base package in XML-based configuration 是强制性的。
以下示例显示了基本包的注释驱动配置:
示例12.基础包的注释驱动配置
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }
存储库代理有两种方法可以从方法名称派生特定于 Store 的查询:
直接从方法名称派生查询。
使用手动定义的查询。
可用选项取决于实际 Store 。但是,必须有一个策略来决定创建实际查询的内容。下一节将介绍可用选项。
存储库基础结构可以使用以下策略来解析查询。使用XML配置,您可以通过 query-lookup-strategy
属性在命名空间配置策略。对于Java配置,您可以使用 Enable${store}Repositories
注释的 queryLookupStrategy
属性。特定数据存储可能不支持某些策略。
CREATE
尝试从查询方法名称构造特定于 Store 的查询。一般方法是从方法名称中删除一组已知的前缀,并解析方法的其余部分。您可以在“ Query Creation ”中阅读有关查询构造的更多信息。
USE_DECLARED_QUERY
尝试查找声明的查询,如果找不到,则抛出异常。查询可以通过某处的注释来定义,也可以通过其他方式声明。查阅特定 Store 的文档以查找该 Store 的可用选项。如果存储库基础结构在引导时未找到该方法的声明查询,则它将失败。
CREATE_IF_NOT_FOUND
(默认)组合 CREATE
和 USE_DECLARED_QUERY
。它首先查找声明的查询,如果没有找到声明的查询,它会创建一个基于自定义方法名称的查询。这是默认的查找策略,因此,如果您未明确配置任何内容,则使用此策略。它允许通过方法名称进行快速查询定义,还可以根据需要引入声明的查询来自定义这些查询。
Spring Data存储库基础结构中内置的查询构建器机制对于构建对存储库实体的约束查询非常有用。该机制从方法中剥离前缀 find…By
,_ 83316, query…By
, count…By
和 get…By
,并开始解析其余部分。 introduction子句可以包含其他表达式,例如 Distinct
,用于在要创建的查询上设置不同的标志。但是,第一个 By
用作分隔符以指示实际条件的开始。在最基本的层面上,您可以在实体属性上定义条件,并将它们与 And
和 Or
连接起来。以下示例显示了如何创建大量查询:
示例13.从方法名称创建查询
interface PersonRepository extends Repository<User, 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);
}
解析方法的实际结果取决于持久性存储为其创建查询。但是,有一些一般要注意的事项:
表达式通常是属性遍历与可以连接的运算符相结合。您可以将属性表达式与 AND
和 OR
组合在一起。您还可以获得对属性表达式的 Between
, LessThan
, GreaterThan
和 Like
等运算符的支持。支持的运算符可能因数据存储而异,因此请参阅参考文档的相应部分。
方法解析器支持为各个属性(例如, findByLastnameIgnoreCase(…)
)或支持忽略大小写的类型的所有属性设置 IgnoreCase
标志(通常为 String
实例 - 例如, findByLastnameAndFirstnameAllIgnoreCase(…)
)。是否支持忽略大小写可能因 Store 而异,因此请参阅参考文档中有关特定于 Store 的查询方法的相关章节。
您可以通过将 OrderBy
子句附加到引用属性的查询方法并提供排序方向( Asc
或 Desc
)来应用静态排序。要创建支持动态排序的查询方法,请参阅“ Special parameter handling ”。
属性表达式只能引用被管实体的直接属性,如前面的示例所示。在创建查询时,您已确保已解析的属性是托管域类的属性。但是,您也可以通过遍历嵌套属性来定义约束。请考虑以下方法签名:
List<Person> findByAddressZipCode(ZipCode zipCode);
假设 Person
的 Address
带有 ZipCode
。在这种情况下,该方法创建属性遍历 x.address.zipCode
。解析算法首先将整个部分( AddressZipCode
)解释为属性,并检查域类中是否具有该名称的属性(未大写)。如果算法成功,它将使用该属性。如果没有,算法将来自右侧的驼峰案例部分的源分成头部和尾部,并尝试找到相应的属性 - 在我们的示例中, AddressZip
和 Code
。如果算法找到具有该头部的属性,则它采用尾部并继续从那里构建树,以刚刚描述的方式将尾部分开。如果第一个分割不匹配,算法会将分割点移动到左侧( Address
, ZipCode
)并继续。
虽然这应该适用于大多数情况,但算法可能会选择错误的属性。假设 Person
类也具有 addressZip
属性。该算法将在第一个拆分轮中匹配,选择错误的属性,并失败(因为 addressZip
的类型可能没有 code
属性)。
要解决这种歧义,可以在方法名称中使用 _
来手动定义遍历点。所以我们的方法名称如下:
List<Person> findByAddress_ZipCode(ZipCode zipCode);
因为我们将下划线字符视为保留字符,所以我们强烈建议遵循标准Java命名约定(即,不在属性名称中使用下划线,而是使用camel case)。
要处理查询中的参数,请定义方法参数,如前面示例中所示。除此之外,基础结构还可识别某些特定类型(如 Pageable
和 Sort
),以动态地对您的查询应用分页和排序。以下示例演示了这些功能:
示例14.在查询方法中使用 Pageable
, Slice
和 Sort
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);
第一种方法允许您将 org.springframework.data.domain.Pageable
实例传递给查询方法,以动态地将分页添加到静态定义的查询中。 Page
知道可用元素和页面的总数。它通过基础设施触发计数查询来计算总数来实现。由于这可能很昂贵(取决于所使用的 Store ),您可以改为返回 Slice
。 Slice
只知道下一个 Slice
是否可用,这在遍历更大的结果集时可能就足够了。
排序选项也通过 Pageable
实例处理。如果只需要排序,请在方法中添加 org.springframework.data.domain.Sort
参数。如您所见,也可以返回 List
。在这种情况下,不会创建构建实际 Page
实例所需的其他元数据(反过来,这意味着不会发出必要的附加计数查询)。相反,它限制查询仅查找给定范围的实体。
要了解整个查询的页数,必须触发其他计数查询。默认情况下,此查询是从您实际触发的查询派生的。
可以使用 first
或 top
关键字来限制查询方法的结果,这些关键字可以互换使用。可选的数值可以附加到 top
或 first
以指定最大结果大小回。如果省略该数字,则假定结果大小为1。以下示例显示如何限制查询大小:
示例15.使用 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
关键字中。
如果将分页或切片应用于限制查询分页(以及可用页数的计算),则将其应用于有限结果中。
通过使用
Sort
参数将结果与动态排序结合使用,可以表达“K”最小元素和“K”元素的查询方法。
返回多个结果的查询方法可以使用标准Java Iterable
, List
, Set
。除此之外,我们支持返回Spring Data的 Streamable
, Iterable
的自定义扩展,以及 Vavr 提供的集合类型。
Streamable
可用作 Iterable
或任何集合类型的替代。它提供了方便的方法来访问非并行 Stream
(从 Iterable
中丢失),能够直接 ….filter(…)
和 ….map(…)
覆盖元素并将 Streamable
连接到其他元素:
示例16.使用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"));
为集合提供专用包装类型是一种常用模式,用于在返回多个元素的查询执行结果上提供API。通常,这些类型通过调用存储库方法来使用,该方法返回类似集合的类型并手动创建包装类型的实例。可以避免这个额外的步骤,因为Spring Data允许使用这些包装类型作为查询方法返回类型,如果它们满足以下标准:
该类型实现 Streamable
。
该类型公开构造函数或名为 of(…)
的静态工厂方法或以 Streamable
为参数的 valueOf(…)
。
示例用例如下所示:
class Product { (1)
MonetaryAmount getPrice() { … }
}
@RequiredArgConstructor(staticName = "of")
class Products implements Streamable<Product> { (2)
private Streamable<Product> streamable;
public MonetaryAmount getTotal() { (3)
return streamable.stream() //
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}
}
interface ProductRepository implements Repository<Product, Long> {
Products findAllByDescriptionContaining(String text); (4)
}
1 | Product 实体,公开API以访问产品的价格。 |
---|---|
2 | Streamable<Product> 的包装类型,可以通过 Products.of(…) (通过Lombok注释创建的工厂方法)构建。 |
3 | 包装类型公开了在 Streamable<Product> 上计算新值的其他API。 |
4 | 该包装类型可以直接用作查询方法返回类型。无需返回 Stremable<Product> 并手动将其包装在存储库客户端中。 |
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
(相当于 Iterable
的Vavr),我们从实际返回值派生实现类,即 java.util.List
将变为Vavr List
/ Seq
, java.util.Set
变为Vavr LinkedHashSet
/ Set
等。
从Spring Data 2.0开始,返回单个聚合实例的存储库CRUD方法使用Java 8的 Optional
来指示可能缺少值。除此之外,Spring Data支持在查询方法上返回以下包装类型:
com.google.common.base.Optional
scala.Option
io.vavr.control.Option
或者,查询方法可以选择根本不使用包装类型。然后通过返回 null
来指示缺少查询结果。保证返回集合,集合替代,包装器和流的存储库方法永远不会返回 null
,而是返回相应的空表示。有关详细信息,请参阅“ Repository query return types ”。
您可以使用 Spring Framework’s nullability annotations 表示存储库方法的可为空性约束。它们提供了一种工具友好的方法,并在运行时选择了 null
检查,如下所示:
@NonNullApi :在包级别上使用,以声明参数和返回值的默认行为是不接受或生成 null
值。
@NonNull :用于参数或返回值不能是 null
(参数和 @NonNullApi
适用的返回值不需要)。
@Nullable :用于可以为 null
的参数或返回值。
Spring注释是使用 JSR 305 annotations进行元注释的(一个休眠但广泛传播的JSR)。 JSR 305元注释允许工具供应商(如 IDEA , Eclipse 和 Kotlin )以通用方式提供空安全支持,而无需对Spring注释进行硬编码支持。要为查询方法启用运行时检查可空性约束,需要在 package-info.java
中使用Spring的 @NonNullApi
来激活包级别的非空性,如以下示例所示:
示例17.在 package-info.java
中声明不可为空性
@org.springframework.lang.NonNullApi
package com.acme;
一旦存在非null默认值,就会在运行时验证存储库查询方法调用的可空性约束。如果查询执行结果违反了定义的约束,则抛出异常。当方法返回 null
但声明为非可空(默认情况下,在存储库所在的包中定义了注释)时会发生这种情况。如果您想再次选择可以为空的结果,请在各个方法上有选择地使用 @Nullable
。使用本节开头提到的结果包装器类型将继续按预期工作:空结果将转换为表示缺席的值。
以下示例显示了刚才描述的一些技术:
示例18.使用不同的可空性约束
package com.acme; (1)
import org.springframework.lang.Nullable;
interface UserRepository extends Repository<User, Long> {
User getByEmailAddress(EmailAddress emailAddress); (2)
@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress); (3)
Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); (4)
}
1 | 存储库位于我们已定义非空行为的包(或子包)中。 |
---|---|
2 | 当执行的查询未产生结果时,抛出 EmptyResultDataAccessException 。当 emailAddress 交给方法 null 时,抛出 IllegalArgumentException 。 |
3 | 当执行的查询未产生结果时,返回 null 。同时接受 null 作为 emailAddress 的值。 |
4 | 当执行的查询未产生结果时返回 Optional.empty() 。当 emailAddress 交给方法 null 时,抛出 IllegalArgumentException 。 |
Kotlin将 nullability constraints 定义为语言。 Kotlin代码编译为字节码,它不通过方法签名表达可空性约束,而是通过编译元数据表达。确保在项目中包含 kotlin-reflect
JAR,以便对Kotlin的可空性约束进行内省。 Spring Data存储库使用语言机制来定义这些约束以应用相同的运行时检查,如下所示:
示例19.对Kotlin存储库使用可空性约束
interface UserRepository : Repository<User, String> {
fun findByUsername(username: String): User (1)
fun findByFirstname(firstname: String?): User? (2)
}
1 | 该方法将参数和结果都定义为非可空(Kotlin默认值)。 Kotlin编译器拒绝将 null 传递给方法的方法调用。如果查询执行产生空结果,则抛出 EmptyResultDataAccessException 。 |
---|---|
2 | 此方法对 firstname 参数接受 null ,如果查询执行未产生结果,则返回 null 。 |
可以使用Java 8 Stream<T>
作为返回类型以递增方式处理查询方法的结果。而不是将查询结果包装在 Stream
数据存储中,特定的方法用于执行流式处理,如以下示例所示:
示例20.使用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()
方法或使用Java 7try-with-resources
块手动关闭Stream
,如以下示例所示:
示例21.在try-with-resources块中使用 Stream<T>
结果
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
并非所有Spring Data模块当前都支持
Stream<T>
作为返回类型。
可以使用 Spring’s asynchronous method execution capability 异步运行存储库查询。这意味着该方法在调用时立即返回,而实际的查询执行发生在已提交给Spring TaskExecutor
的任务中。异步查询执行与反应式查询执行不同,不应混合使用。有关反应支持的更多详细信息,请参阅特定于 Store 的文档。以下示例显示了许多异步查询:
@Async
Future<User> findByFirstname(String firstname); (1)
@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
@Async
ListenableFuture<User> findOneByLastname(String lastname); (3)
1 | 使用 java.util.concurrent.Future 作为返回类型。 |
---|---|
2 | 使用Java 8 java.util.concurrent.CompletableFuture 作为返回类型。 |
3 | 使用 org.springframework.util.concurrent.ListenableFuture 作为返回类型。 |
在本节中,您将创建实例和已定义的存储库接口的bean定义。一种方法是使用随每个支持存储库机制的Spring Data模块一起提供的Spring命名空间,尽管我们通常建议使用Java配置。
每个Spring Data模块都包含一个 repositories
元素,允许您定义Spring为您扫描的基础包,如以下示例所示:
示例22.通过XML启用Spring Data存储库
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<repositories base-package="com.acme.repositories" />
</beans:beans>
在前面的示例中,指示Spring扫描 com.acme.repositories
及其所有子包,以查找扩展 Repository
或其子接口之一的接口。对于找到的每个接口,基础结构都会注册特定于持久性技术的 FactoryBean
,以创建处理查询方法调用的相应代理。每个bean都是在从接口名称派生的bean名称下注册的,因此 UserRepository
的接口将在 userRepository
下注册。 base-package
属性允许使用通配符,以便您可以定义扫描包的模式。
默认情况下,基础结构会选择扩展位于已配置的基本软件包下的特定于持久性技术的子接口的每个接口,并为其创建一个bean实例。但是,您可能希望对哪些接口为其创建bean实例进行更细粒度的控制。为此,请在 <repositories />
元素内使用 <include-filter />
和 <exclude-filter />
个元素。语义完全等同于Spring的上下文命名空间中的元素。有关详细信息,请参阅 Spring reference documentation 以了解这些元素。
例如,要将某些接口从实例化中排除为存储库bean,可以使用以下配置:
示例23.使用exclude-filter元素
<repositories base-package="com.acme.repositories">
<context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>
前面的示例排除了以 SomeRepository
结尾的所有接口的实例化。
还可以通过在JavaConfig类上使用特定于存储的 @Enable${store}Repositories
注释来触发存储库基础结构。有关Spring容器的基于Java的配置的介绍,请参阅 JavaConfig in the Spring reference documentation 。
启用Spring Data存储库的示例配置类似于以下内容:
示例24.基于样本注释的存储库配置
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
前面的示例使用JPA特定的注释,您可以根据实际使用的 Store 模块进行更改。这同样适用于
EntityManagerFactory
bean的定义。请参阅涵盖特定于 Store 的配置的部分。
您还可以在Spring容器之外使用存储库基础结构 - 例如,在CDI环境中。您仍然需要在类路径中使用一些Spring库,但通常也可以通过编程方式设置存储库。提供存储库支持的Spring Data模块提供了一个特定于持久性技术的 RepositoryFactory
,您可以按如下方式使用它:
示例25.存储库工厂的独立使用
RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);
本节介绍存储库自定义以及片段如何构成复合存储库。
当查询方法需要不同的行为或无法通过查询派生实现时,则需要提供自定义实现。 Spring Data存储库允许您提供自定义存储库代码,并将其与通用CRUD抽象和查询方法功能集成。
要使用自定义功能丰富存储库,必须首先定义片段接口和自定义功能的实现,如以下示例所示:
示例26.自定义存储库功能的接口
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
然后,您可以让您的存储库接口从片段接口进一步扩展,如以下示例所示:
示例27.自定义存储库功能的实现
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void someCustomMethod(User user) {
// Your custom implementation
}
}
对应于片段接口的类名最重要的部分是
Impl
postfix。
实现本身不依赖于Spring Data,可以是常规的Spring bean。因此,您可以使用标准依赖项注入行为来注入对其他bean的引用(例如 JdbcTemplate
),参与方面等等。
您可以让存储库接口扩展片段接口,如以下示例所示:
示例28.对存储库界面的更改
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {
// Declare query methods here
}
使用存储库接口扩展片段接口可以组合CRUD和自定义功能,并使其可供客户端使用。
Spring Data存储库通过使用形成存储库组合的片段来实现。片段是基本存储库,功能方面(例如 QueryDsl ),以及自定义接口及其实现。每次向存储库界面添加接口时,您都可以通过添加片段来增强组成。每个Spring Data模块都提供了基本存储库和存储库方面的实现。
以下示例显示了自定义接口及其实现:
示例29.具有其实现的片段
interface HumanRepository {
void someHumanMethod(User user);
}
class HumanRepositoryImpl implements HumanRepository {
public void someHumanMethod(User user) {
// Your custom implementation
}
}
interface ContactRepository {
void someContactMethod(User user);
User anotherContactMethod(User user);
}
class ContactRepositoryImpl implements ContactRepository {
public void someContactMethod(User user) {
// Your custom implementation
}
public User anotherContactMethod(User user) {
// Your custom implementation
}
}
以下示例显示了扩展 CrudRepository
的自定义存储库的接口:
示例30.对存储库界面的更改
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {
// Declare query methods here
}
存储库可以由多个自定义实现组成,这些实现按其声明的顺序导入。自定义实现的优先级高于基本实现和存储库方面。如果两个片段提供相同的方法签名,则此排序允许您覆盖基本存储库和方面方法并解决歧义。存储库片段不限于在单个存储库接口中使用。多个存储库可以使用片段接口,允许您跨不同的存储库重用自定义。
以下示例显示了存储库片段及其实现:
示例31.覆盖 save(…)
的碎片
interface CustomizedSave<T> {
<S extends T> S save(S entity);
}
class CustomizedSaveImpl<T> implements CustomizedSave<T> {
public <S extends T> S save(S entity) {
// Your custom implementation
}
}
以下示例显示了使用前面的存储库片段的存储库:
示例32.定制的存储库接口
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}
interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
如果使用命名空间配置,则存储库基础结构会尝试通过扫描其找到存储库的包下面的类来自动检测自定义实现片段。这些类需要遵循将命名空间元素的 repository-impl-postfix
属性附加到片段接口名称的命名约定。此后缀默认为 Impl
。以下示例显示了使用默认后缀的存储库以及为后缀设置自定义值的存储库:
示例33.配置示例
<repositories base-package="com.acme.repository" />
<repositories base-package="com.acme.repository" repository-impl-postfix="MyPostfix" />
上一个示例中的第一个配置尝试查找名为 com.acme.repository.CustomizedUserRepositoryImpl
的类,以充当自定义存储库实现。第二个示例尝试查找 com.acme.repository.CustomizedUserRepositoryMyPostfix
。
如果在不同的包中找到具有匹配类名的多个实现,则Spring Data使用bean名称来标识要使用的名称。
给定前面显示的 CustomizedUserRepository
的以下两个自定义实现,使用第一个实现。它的bean名称是 customizedUserRepositoryImpl
,它与片段接口( CustomizedUserRepository
)加上后缀 Impl
相匹配。
示例34.无差别实现的解决方案
package com.acme.impl.one;
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
package com.acme.impl.two;
@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
// Your custom implementation
}
如果使用 @Component("specialCustom")
注释 UserRepository
接口,则bean名称加 Impl
将匹配 com.acme.impl.two
中为存储库实现定义的名称,并使用它而不是第一个。
如果您的自定义实现仅使用基于注释的配置和自动装配,则前面显示的方法效果很好,因为它被视为任何其他Spring bean。如果您的实现片段bean需要特殊连接,您可以声明bean并根据 preceding section 中描述的约定对其进行命名。然后,基础结构按名称引用手动定义的bean定义,而不是自己创建一个。以下示例显示如何手动连接自定义实现:
示例35.自定义实现的手动连接
<repositories base-package="com.acme.repository" />
<beans:bean id="userRepositoryImpl" class="…">
<!-- further configuration -->
</beans:bean>
当您要自定义基本存储库行为以便所有存储库都受到影响时, preceding section 中描述的方法需要自定义每个存储库接口。要改为更改所有存储库的行为,可以创建一个扩展特定于持久性技术的存储库基类的实现。然后,此类充当存储库代理的自定义基类,如以下示例所示:
示例36.自定义存储库基类
class MyRepositoryImpl<T, ID>
extends SimpleJpaRepository<T, ID> {
private final EntityManager entityManager;
MyRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
// Keep the EntityManager around to used from the newly introduced methods.
this.entityManager = entityManager;
}
@Transactional
public <S extends T> S save(S entity) {
// implementation goes here
}
}
该类需要具有特定于 Store 的存储库工厂实现所使用的超类的构造函数。如果存储库基类具有多个构造函数,则覆盖采用
EntityInformation
以及特定于存储的基础结构对象(例如EntityManager
或模板类)的构造函数。
最后一步是使Spring Data Infrastructure了解自定义存储库基类。在Java配置中,您可以使用 @Enable${store}Repositories
注释的 repositoryBaseClass
属性来执行此操作,如以下示例所示:
示例37.使用JavaConfig配置自定义存储库基类
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
XML命名空间中提供了相应的属性,如以下示例所示:
示例38.使用XML配置自定义存储库基类
<repositories base-package="com.acme.repository"
base-class="….MyRepositoryImpl" />
由存储库管理的实体是聚合根。在域驱动设计应用程序中,这些聚合根通常会发布域事件。 Spring Data提供了一个名为 @DomainEvents
的注释,您可以在聚合方法上使用它root以使该发布尽可能简单,如以下示例所示:
示例39.从聚合根公开事件域事件
class AnAggregateRoot {
@DomainEvents (1)
Collection<Object> domainEvents() {
// … return events you want to get published here
}
@AfterDomainEventPublication (2)
void callbackMethod() {
// … potentially clean up domain events list
}
}
1 | 使用 @DomainEvents 的方法可以返回单个事件实例或事件集合。它不能采取任何论点。 |
---|---|
2 | 发布所有事件后,我们有一个用 @AfterDomainEventPublication 注释的方法。它可用于潜在地清除要发布的事件列表(以及其他用途)。 |
每次调用Spring Data存储库的 save(…)
方法时,都会调用这些方法。
本节介绍了一组Spring Data扩展,它们可以在各种上下文中使用Spring Data。目前,大多数集成都针对Spring MVC。
Querydsl 是一个框架,可以通过其流畅的API构建静态类型的类SQL查询。
几个Spring Data模块通过 QuerydslPredicateExecutor
提供与Querydsl的集成,如以下示例所示:
示例40.QuerydslPredicateExecutor接口
public interface QuerydslPredicateExecutor<T> {
Optional<T> findById(Predicate predicate); (1)
Iterable<T> findAll(Predicate predicate); (2)
long count(Predicate predicate); (3)
boolean exists(Predicate predicate); (4)
// … more functionality omitted.
}
1 | 查找并返回与 Predicate 匹配的单个实体。 |
---|---|
2 | 查找并返回与 Predicate 匹配的所有实体。 |
3 | 返回与 Predicate 匹配的实体数。 |
4 | 返回是否存在与 Predicate 匹配的实体。 |
要使用Querydsl支持,请在存储库接口上扩展 QuerydslPredicateExecutor
,如以下示例所示
示例41.存储库上的Querydsl集成
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
上面的示例允许您使用Querydsl Predicate
实例编写类型安全查询,如以下示例所示:
Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
本节包含Spring Data Web支持的文档,因为它在Spring Data Commons的当前(及更高版本)版本中实现。由于新引入的支持改变了很多东西,我们在 [web.legacy] 中保留了以前行为的文档。
支持存储库编程模型的Spring Data模块具有各种Web支持。 Web相关组件要求Spring MVC JAR位于类路径上。其中一些甚至提供与 Spring HATEOAS 的集成。通常,通过在JavaConfig配置类中使用 @EnableSpringDataWebSupport
批注来启用集成支持,如以下示例所示:
示例42.启用Spring Data Web支持
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
@EnableSpringDataWebSupport
注释会记录我们稍后将讨论的一些组件。它还将检测类路径上的Spring HATEOAS,并为它注册集成组件(如果存在)。
或者,如果使用XML配置,请将 SpringDataWebConfiguration
或 HateoasAwareSpringDataWebConfiguration
注册为Spring bean,如以下示例所示(对于 SpringDataWebConfiguration
):
示例43.在XML中启用Spring Data Web支持
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
<!-- If you use Spring HATEOAS, register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />
previous section 中显示的配置注册了一些基本组件:
A DomainClassConverter 让Spring MVC从请求参数或路径变量中解析存储库管理的域类的实例。
HandlerMethodArgumentResolver 实现让Spring MVC从请求参数中解析 Pageable
和 Sort
实例。
DomainClassConverter
允许您直接在Spring MVC控制器方法签名中使用域类型,因此您无需通过存储库手动查找实例,如以下示例所示:
示例44.在方法签名中使用域类型的Spring MVC控制器
@Controller
@RequestMapping("/users")
class UserController {
@RequestMapping("/{id}")
String showUserForm(@PathVariable("id") User user, Model model) {
model.addAttribute("user", user);
return "userForm";
}
}
如您所见,该方法直接接收 User
实例,无需进一步查找。可以通过让Spring MVC首先将路径变量转换为域类的 id
类型来解析实例,并最终通过在为域类型注册的存储库实例上调用 findById(…)
来访问实例。
目前,存储库必须实现
CrudRepository
才有资格被发现进行转换。
previous section 中显示的配置片段还注册 PageableHandlerMethodArgumentResolver
以及 SortHandlerMethodArgumentResolver
的实例。注册启用 Pageable
和 Sort
作为有效的控制器方法参数,如以下示例所示:
示例45.使用Pageable作为控制器方法参数
@Controller
@RequestMapping("/users")
class UserController {
private final UserRepository repository;
UserController(UserRepository repository) {
this.repository = repository;
}
@RequestMapping
String showUsers(Model model, Pageable pageable) {
model.addAttribute("users", repository.findAll(pageable));
return "users";
}
}
前面的方法签名导致Spring MVC尝试使用以下默认配置从请求参数派生 Pageable
实例:
page | 要检索的页面。 0索引并默认为0. |
---|---|
size | 要检索的页面大小。默认为20. |
sort | 应按 property,property(,ASC|DESC) 格式排序的属性。默认排序方向是升序。如果要切换方向,请使用多个 sort 参数 - 例如, ?sort=firstname&sort=lastname,asc 。 |
要自定义此行为,请分别注册实现 PageableHandlerMethodArgumentResolverCustomizer
接口或 SortHandlerMethodArgumentResolverCustomizer
接口的bean。调用其 customize()
方法,让您更改设置,如以下示例所示:
@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
return s -> s.setPropertyDelimiter("<-->");
}
如果设置现有 MethodArgumentResolver
的属性不足以满足您的需要,请扩展 SpringDataWebConfiguration
或启用HATEOAS的等效项,覆盖 pageableResolver()
或 sortResolver()
方法,并导入自定义配置文件,而不是使用 @Enable
注释。
如果需要从请求中解析多个 Pageable
或 Sort
实例(例如,对于多个表),可以使用Spring的 @Qualifier
注释来区分彼此。然后,请求参数必须以 ${qualifier}_
作为前缀。以下示例显示了生成的方法签名:
String showUsers(Model model,
@Qualifier("thing1") Pageable first,
@Qualifier("thing2") Pageable second) { … }
你必须填充 thing1_page
和 thing2_page
等等。
传递给方法的默认 Pageable
等效于 PageRequest.of(0, 20)
,但可以使用 Pageable
参数上的 @PageableDefault
注释进行自定义。
Spring HATEOAS附带了一个表示模型类( PagedResources
),它允许使用必要的 Page
元数据丰富 Page
实例的内容以及允许客户端轻松浏览页面的链接。将Page转换为 PagedResources
是由Spring HATEOAS ResourceAssembler
接口的实现完成的,称为 PagedResourcesAssembler
。以下示例显示如何将 PagedResourcesAssembler
用作控制器方法参数:
示例46.使用PagedResourcesAssembler作为控制器方法参数
@Controller
class PersonController {
@Autowired PersonRepository repository;
@RequestMapping(value = "/persons", method = RequestMethod.GET)
HttpEntity<PagedResources<Person>> persons(Pageable pageable,
PagedResourcesAssembler assembler) {
Page<Person> persons = repository.findAll(pageable);
return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
}
}
如上例所示启用配置可以将 PagedResourcesAssembler
用作控制器方法参数。在其上调用 toResources(…)
具有以下效果:
Page
的内容成为 PagedResources
实例的内容。
PagedResources
对象附加 PageMetadata
实例,并使用 Page
和基础 PageRequest
中的信息填充。
PagedResources
可能会附加 prev
和 next
个链接,具体取决于页面的状态。链接指向方法映射到的URI。添加到方法的分页参数与 PageableHandlerMethodArgumentResolver
的设置相匹配,以确保稍后可以解析链接。
假设我们在数据库中有30个Person实例。您现在可以触发请求( GET http://localhost:8080/persons
)并查看类似于以下内容的输出:
{ "links" : [ { "rel" : "next",
"href" : "http://localhost:8080/persons?page=1&size=20 }
],
"content" : [
… // 20 Person instances rendered here
],
"pageMetadata" : {
"size" : 20,
"totalElements" : 30,
"totalPages" : 2,
"number" : 0
}
}
您会看到汇编程序生成了正确的URI,并且还选择了默认配置以将参数解析为 Pageable
以用于即将发生的请求。这意味着,如果更改该配置,链接将自动遵循更改。默认情况下,汇编程序指向它所调用的控制器方法,但可以通过将自定义 Link
作为基础来定制,以构建分页链接,从而使 PagedResourcesAssembler.toResource(…)
方法重载。
Spring数据投影(在 [projections] 中描述)可用于通过使用 JSONPath 表达式来绑定传入的请求有效负载(需要 Jayway JsonPath 或 XPath 表达式(需要 XmlBeam ),如以下示例所示:
示例47.使用JSONPath或XPath表达式的HTTP有效负载绑定
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
前面示例中显示的类型可以用作Spring MVC处理程序方法参数,也可以在 RestTemplate
方法之一上使用 ParameterizedTypeReference
。前面的方法声明将尝试在给定文档中的任何位置查找 firstname
。 lastname
XML查找在传入文档的顶级执行。其中JSON变体首先尝试顶级 lastname
,但如果前者未返回值,则还会尝试嵌套在 user
子文档中的 lastname
。这样,可以轻松地减轻源文档结构的变化,而无需客户端调用公开的方法(通常是基于类的有效负载绑定的缺点)。
支持嵌套投影,如 [projections] 中所述。如果该方法返回复杂的非接口类型,则使用Jackson ObjectMapper
来映射最终值。
对于Spring MVC,只要 @EnableSpringDataWebSupport
处于活动状态,就会自动注册必要的转换器,并且类路径上可以使用所需的依赖项。要与 RestTemplate
一起使用,请手动注册 ProjectingJackson2HttpMessageConverter
(JSON)或 XmlBeamHttpMessageConverter
。
有关更多信息,请参阅规范 Spring Data Examples repository 中的 web projection example 。
对于具有 QueryDSL 集成的那些 Store ,可以从 Request
查询字符串中包含的属性派生查询。
请考虑以下查询字符串:
?firstname=Dave&lastname=Matthews
鉴于 User
从前面的示例中的对象,可以使用 QuerydslPredicateArgumentResolver
将查询字符串解析为以下值。
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
在类路径中找到Querydsl时,该功能与
@EnableSpringDataWebSupport
一起自动启用。
在方法签名中添加 @QuerydslPredicate
可提供即用型 Predicate
,可以使用 QuerydslPredicateExecutor
运行。
类型信息通常从方法的返回类型中解析。由于该信息不一定与域类型匹配,因此使用
QuerydslPredicate
的root
属性可能是个好主意。
以下示例说明如何在方法签名中使用 @QuerydslPredicate
:
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate, (1)
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
1 | 将查询字符串参数解析为 Predicate 匹配 User 。 |
---|
默认绑定如下:
Object
在简单属性上为 eq
。
Object
关于集合,如 contains
属性。
Collection
在简单属性上为 in
。
可以通过 @QuerydslPredicate
的 bindings
属性或通过使用Java 8 default methods
并将 QuerydslBinderCustomizer
方法添加到存储库接口来自定义这些绑定。
interface UserRepository extends CrudRepository<User, String>,
QuerydslPredicateExecutor<User>, (1)
QuerydslBinderCustomizer<QUser> { (2)
@Override
default void customize(QuerydslBindings bindings, QUser user) {
bindings.bind(user.username).first((path, value) -> path.contains(value)) (3)
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value)); (4)
bindings.excluding(user.password); (5)
}
}
1 | QuerydslPredicateExecutor 提供对 Predicate 的特定查找程序方法的访问。 | |
---|---|---|
存储库界面上定义的 | 2 | QuerydslBinderCustomizer 会自动获取并快捷方式 @QuerydslPredicate(bindings=…) 。 |
3 | 将 username 属性的绑定定义为简单的 contains 绑定。 | |
4 | 将 String 属性的默认绑定定义为不区分大小写的 contains 匹配。 | |
5 | 从 Predicate 分辨率中排除 password 属性。 |
如果您使用Spring JDBC模块,您可能熟悉使用SQL脚本填充 DataSource
的支持。虽然它不使用SQL作为数据定义语言,但它在存储库级别上可以使用类似的抽象,因为它必须与存储无关。因此,填充程序支持XML(通过Spring的OXM抽象)和JSON(通过Jackson)来定义用于填充存储库的数据。
假设您有一个文件 data.json
,其中包含以下内容:
示例48.在JSON中定义的数据
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
您可以使用Spring Data Commons中提供的存储库命名空间的populator元素来填充存储库。要将前面的数据填充到PersonRepository,请声明类似于以下内容的populator:
示例49.声明Jackson存储库填充程序
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
http://www.springframework.org/schema/data/repository/spring-repository.xsd">
<repository:jackson2-populator locations="classpath:data.json" />
</beans>
前面的声明导致 data.json
文件被Jackson ObjectMapper
读取和反序列化。
通过检查JSON文档的 _class
属性来确定解组JSON对象的类型。基础结构最终选择适当的存储库来处理反序列化的对象。
要使用XML来定义应该填充存储库的数据,可以使用 unmarshaller-populator
元素。您将其配置为使用Spring OXM中提供的XML marshaller选项之一。有关详细信息,请参阅 Spring reference documentation 。以下示例说明如何使用JAXB解组存储库填充程序:
示例50.声明一个解组存储库populator(使用JAXB)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xmlns:oxm="http://www.springframework.org/schema/oxm"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
http://www.springframework.org/schema/data/repository/spring-repository.xsd
http://www.springframework.org/schema/oxm
http://www.springframework.org/schema/oxm/spring-oxm.xsd">
<repository:unmarshaller-populator locations="classpath:data.json"
unmarshaller-ref="unmarshaller" />
<oxm:jaxb2-marshaller contextPath="com.acme" />
</beans>
本章指出了JDBC的存储库支持的特性。这基于 Working with Spring Data Repositories 中解释的核心存储库支持。你应该对那里解释的基本概念有充分的理解。
Java世界中关系数据库的主要持久性API当然是JPA,它有自己的Spring Data模块。为什么还有另一个?
为了帮助开发人员,JPA做了很多事情。除此之外,它还跟踪实体的变化。它为您提供延迟加载。它允许您将各种对象构造映射到同样广泛的数据库设计。
这很棒,让很多事情变得非常简单。只需看一下基本的JPA教程。但是,为什么JPA会做某件事常常让人感到困惑。而且,JPA在概念上非常简单。
Spring Data JDBC通过采用以下设计决策,旨在简化概念:
如果加载实体,则执行SQL语句。完成此操作后,您将拥有一个完全加载的实体。没有延迟加载或缓存。
如果您保存实体,它将被保存。如果你不这样做,那就没有。没有脏跟踪和没有会话。
有一个如何将实体映射到表的简单模型。它可能仅适用于相当简单的情况。如果你不喜欢这样,你应该编写自己的策略。 Spring Data JDBC仅提供非常有限的支持使用注释自定义策略。
所有Spring Data模块都受到Domain Driven Design中“repository”,“aggregate”和“aggregate root”概念的启发。对于Spring Data JDBC来说,这些可能更为重要,因为在使用关系数据库时,它们在某种程度上与正常做法相反。
聚合是一组实体,它们保证在原子更改之间保持一致。一个典型的例子是带有 OrderItems
的 Order
。 Order
上的属性(例如, numberOfItems
与 OrderItems
的实际数量一致)在进行更改时保持一致。
跨聚合的引用不保证始终保持一致。它们最终会保持一致。
每个聚合只有一个聚合根,它是聚合的一个实体。聚合只能通过该聚合根上的方法进行操作。这些是前面提到的原子变化。
存储库是持久存储的抽象,它看起来像某个类型的所有聚合的集合。对于一般的Spring数据,这意味着您希望每个聚合根有一个 Repository
。此外,对于Spring Data JDBC,这意味着从聚合根可访问的所有实体都被视为该聚合根的一部分。 Spring Data JDBC假定只有聚合具有存储聚合的非根实体的表的外键,而没有其他实体指向非根实体。
在当前实现中,从聚合根引用的实体将被Spring Data JDBC删除并重新创建。
您可以使用与您的工作方式和数据库设计相匹配的实现来覆盖存储库方法。
可以通过Java配置通过注释激活Spring Data JDBC存储库支持,如以下示例所示:
示例51.使用Java配置的Spring Data JDBC存储库
@Configuration
@EnableJdbcRepositories
class ApplicationConfig {
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.HSQL).build();
}
}
上例中的配置类使用 spring-jdbc
的 EmbeddedDatabaseBuilder
API设置嵌入式HSQL数据库。我们使用 @EnableJdbcRepositories
激活Spring Data JDBC存储库。如果未配置基本软件包,则使用配置类所在的软件包。
可以使用 CrudRepository.save(…)
方法执行保存聚合。如果聚合是新的,则会导致聚合根的插入,然后是所有直接或间接引用的实体的插入语句。
如果聚合根不是新的,则删除所有引用的实体,更新聚合根,并再次插入所有引用的实体。请注意,实例是否为新实例是实例状态的一部分。
这种方法有一些明显的缺点。如果实际上只改变了很少的引用实体,则删除和插入是浪费的。虽然这个过程可能并且可能会得到改进,但Spring Data JDBC可以提供一些限制。它不知道聚合的先前状态。因此,任何更新过程总是必须采取它在数据库中找到的任何内容,并确保将其转换为传递给save方法的实体的状态。
本节介绍Spring Data对象映射,对象创建,字段和属性访问,可变性和不变性的基础知识。注意,本节仅适用于不使用底层数据存储的对象映射的Spring Data模块(如JPA)。另外,请务必查阅特定于 Store 的部分以了解特定于 Store 的对象映射,例如索引,自定义列或字段名称等。
Spring Data对象映射的核心职责是创建域对象的实例,并将存储本机数据结构映射到这些对象上。这意味着我们需要两个基本步骤:
使用其中一个公开的构造函数创建实例。
要实现所有公开属性的实例填充。
Spring Data会自动尝试检测持久化实体的构造函数,以用于实现该类型的对象。分辨率算法的工作原理如下:
如果有一个无参数的构造函数,它将被使用。其他构造函数将被忽略。
如果有一个构造函数接受参数,它将被使用。
如果有多个构造函数接受参数,则Spring Data要使用的构造函数必须使用 @PersistenceConstructor
进行注释。
值解析假定构造函数参数名称与实体的属性名称匹配,即将执行解析,就像要填充属性一样,包括映射中的所有自定义(不同的数据存储列或字段名称等)。这也需要参数名称类文件中可用的信息或构造函数中存在的 @ConstructorProperties
注释。
可以使用Spring Framework的 @Value
值注释使用特定于 Store 的SpEL表达式来自定义值解析。有关更多详细信息,请参阅有关 Store 特定映射的部分。
对象创建内部
为了避免反射的开销,Spring Data对象创建使用默认情况下在运行时生成的工厂类,它将直接调用域类构造函数。即对于此示例类型:
class Person {
Person(String firstname, String lastname) { … }
}
我们将在运行时创建一个在语义上等效于此工厂类的工厂类:
class PersonObjectInstantiator implements ObjectInstantiator {
Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}
这使得我们在反射方面的性能提升了10%。要使域类符合此类优化的条件,它需要遵守一组约束:
它不能是私人类
它不能是非静态内部类
它不能是CGLib代理类
Spring Data使用的构造函数不能是私有的
如果这些条件中的任何一个匹配,Spring Data将通过反射回退到实体实例化。
一旦创建了实体的实例,Spring Data就会填充该类的所有剩余持久属性。除非已经由实体的构造函数填充(即通过其构造函数参数列表使用),否则将首先填充identifier属性以允许循环对象引用的解析。之后,在实体实例上设置尚未由构造函数填充的所有非瞬态属性。为此,我们使用以下算法:
如果属性是不可变的但是暴露了一个wither方法(见下文),我们使用wither来创建一个具有新属性值的新实体实例。
如果定义了属性访问(即通过getter和setter访问),我们将调用setter方法。
默认情况下,我们直接设置字段值。
property 人口内部
与我们的 optimizations in object construction 类似,我们还使用Spring Data运行时生成的访问器类与实体实例进行交互。
class Person {
private final Long id;
private String firstname;
private @AccessType(Type.PROPERTY) String lastname;
Person() {
this.id = null;
}
Person(Long id, String firstname, String lastname) {
// Field assignments
}
Person withId(Long id) {
return new Person(id, this.firstname, this.lastame);
}
void setLastname(String lastname) {
this.lastname = lastname;
}
}
示例52.生成的属性访问器
class PersonPropertyAccessor implements PersistentPropertyAccessor {
private static final MethodHandle firstname; (2)
private Person person; (1)
public void setProperty(PersistentProperty property, Object value) {
String name = property.getName();
if ("firstname".equals(name)) {
firstname.invoke(person, (String) value); (2)
} else if ("id".equals(name)) {
this.person = person.withId((Long) value); (3)
} else if ("lastname".equals(name)) {
this.person.setLastname((String) value); (4)
}
}
}
1 | PropertyAccessor持有底层对象的可变实例。这是为了实现其他不可变属性的突变。 |
---|---|
2 | 默认情况下,Spring Data使用字段访问来读取和写入属性值。根据 private 字段的可见性规则, MethodHandles 用于与字段交互。 |
3 | 该类公开了一个用于设置标识符的 withId(…) 方法,例如将实例插入数据存储区并生成标识符时。调用 withId(…) 会创建一个新的 Person 对象。所有后续突变都将在新实例中发生,而前一个突变不会发生。 |
4 | 使用property-access允许直接方法调用而不使用 MethodHandles 。 |
这使我们在反射方面的性能提升了25%。要使域类符合此类优化的条件,它需要遵守一组约束:
类型不得位于默认值或 java
包下。
类型及其构造函数必须为 public
内部类的类型必须是 static
。
使用的Java运行时必须允许在原始 ClassLoader
中声明类。 Java 9和更新版本施加了某些限制。
默认情况下,如果检测到限制,Spring Data会尝试使用生成的属性访问器并回退到基于反射的访问器。
我们来看看以下实体:
例53.一个样本实体
class Person {
private final @Id Long id; (1)
private final String firstname, lastname; (2)
private final LocalDate birthday;
private final int age; (3)
private String comment; (4)
private @AccessType(Type.PROPERTY) String remarks; (5)
static Person of(String firstname, String lastname, LocalDate birthday) { (6)
return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { (6)
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}
Person withId(Long id) { (1)
return new Person(id, this.firstname, this.lastname, this.birthday);
}
void setRemarks(String remarks) { (5)
this.remarks = remarks;
}
}
1 | 标识符属性为final,但在构造函数中设置为 null 。该类公开了一个用于设置标识符的 withId(…) 方法,例如将实例插入数据存储区并生成标识符时。原始 Person 实例在创建新实例时保持不变。相同的模式通常应用于存储管理的其他属性,但可能必须更改以进行持久性操作。 | |
---|---|---|
2 | firstname 和 lastname 属性是可能通过getter公开的普通不可变属性。 | |
3 | age 属性是 birthday 属性中的不可变但派生的属性。在显示设计的情况下,数据库值将胜过默认值,因为Spring Data使用唯一声明的构造函数。即使意图是计算应该是首选,重要的是这个构造函数也将 age 作为参数(可能忽略它),否则属性填充步骤将尝试设置age字段并因为它是不可变的而失败。枯萎的存在。 | |
4 | comment 属性是可变的,通过直接设置其字段来填充。 | |
5 | remarks 属性是可变的,并通过直接设置 comment 字段或通过调用 | 的setter方法来填充 |
6 | 该类公开了一个工厂方法和一个用于创建对象的构造函数。这里的核心思想是使用工厂方法而不是其他构造函数来避免构造函数通过 @PersistenceConstructor 消除歧义。相反,在工厂方法中处理属性的默认值。 |
尝试坚持不可变对象 - 不可变对象很容易创建,因为实现对象只是调用它的构造函数。此外,这可以避免使用允许客户端代码操纵对象状态的setter方法来填充域对象。如果您需要这些,请更喜欢使它们受到保护,以便只能通过有限数量的共存类型来调用它们。仅构造函数的实现比属性总体快30%。
提供一个all-args构造函数 - 即使你不能或不想将你的实体建模为不可变值,提供一个构造函数仍然是有 Value 的,该构造函数将实体的所有属性作为参数,包括可变属性,因为这允许对象映射跳过属性填充以获得最佳性能。
使用工厂方法而不是重载的构造函数来避免 @PersistenceConstructor
- 为了获得最佳性能所需的全参数构造函数,我们通常希望公开更多的应用程序用例特定构造函数,这些构造函数省略了自动生成的标识符等内容。这是一种既定的模式使用静态工厂方法来公开all-args构造函数的这些变体。
确保遵守允许使用生成的实例化器和属性访问器类的约束 -
对于要生成的标识符,仍将最终字段与枯萎方法结合使用 -
使用Lombok来避免样板代码 - 由于持久性操作通常需要构造函数接受所有参数,因此它们的声明变成了对字段赋值的样板参数的繁琐重复,使用Lombok的 @AllArgsConstructor
可以最好地避免这种重复。
Spring Data调整Kotlin的细节以允许对象创建和变异。
支持实例化Kotlin类,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。考虑以下 data
class Person
:
data class Person(val id: String, val name: String)
上面的类使用显式构造函数编译为典型的类。我们可以通过添加另一个构造函数来自定义此类,并使用 @PersistenceConstructor
注释它以指示构造函数首选项:
data class Person(var id: String, val name: String) {
@PersistenceConstructor
constructor(id: String) : this(id, "unknown")
}
如果未提供参数,则允许使用默认值,Kotlin支持参数选项。当Spring Data检测到具有参数默认值的构造函数时,如果数据存储没有提供值(或者只是返回 null
),则它将不存在这些参数,因此Kotlin可以应用参数默认值。考虑以下为 name
应用参数默认值的类
data class Person(var id: String, val name: String = "unknown")
每次 name
参数不是结果的一部分或其值是 null
时, name
默认为 unknown
。
在Kotlin中,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。考虑以下 data
class Person
:
data class Person(val id: String, val name: String)
这个类实际上是不可变的。它允许创建新实例,因为Kotlin生成一个 copy(…)
方法,该方法创建新对象实例,从现有对象复制所有属性值,并将作为参数提供的属性值应用于该方法。
目前支持以下类型的属性:
所有基本类型及其盒装类型( int
, float
, Integer
, Float
等)
枚举被映射到他们的名字。
String
java.util.Date
, java.time.LocalDate
, java.time.LocalDateTime
和 java.time.LocalTime
数据库驱动程序接受的任何内容。
对其他实体的引用。他们被认为是一对一的关系。这些实体具有 id
属性是可选的。引用实体的表应该具有与引用实体的表相同的附加列。您可以通过实现 NamingStrategy.getReverseColumnName(RelationalPersistentProperty property)
来更改此名称。
Set<some entity>
被认为是一对多的关系。引用实体的表应该具有与引用实体的表相同的附加列。您可以通过实现 NamingStrategy.getReverseColumnName(RelationalPersistentProperty property)
来更改此名称。
Map<simple type, some entity>
被认为是合格的一对多关系。引用实体的表应该有两个附加列:一个名称与外键的引用实体的表相同,另一个名称相同,并且映射键的后缀为 _key
。您可以分别通过实现 NamingStrategy.getReverseColumnName(RelationalPersistentProperty property)
和 NamingStrategy.getKeyColumn(RelationalPersistentProperty property)
来更改此行为。或者,您可以使用 @Column(value="your_column_name", keyColumn="your_key_column_name")
注释属性
List<some entity>
被映射为 Map<Integer, some entity>
。
引用实体的处理是有限的。这是基于如上所述的聚合根的想法。如果您引用另一个实体,则根据定义,该实体是您的聚合的一部分。因此,如果删除引用,则先前引用的实体将被删除。这也意味着参考是1-1或1-n,但不是n-1或n-m。
如果您有n-1或n-m引用,则根据定义,您将处理两个单独的聚合。它们之间的引用应编码为简单的 id
值,这些值应与Spring Data JDBC正确映射。
通过从 JdbcConfiguration
继承配置并覆盖方法 jdbcCustomConversions()
,可以为默认情况下不支持的类型注册自定义转换器。
@Configuration
public class DataJdbcConfiguration extends JdbcConfiguration {
@Override
protected JdbcCustomConversions jdbcCustomConversions() {
return new JdbcCustomConversions(Collections.singletonList(TimestampTzToDateConverter.INSTANCE));
}
@ReadingConverter
enum TimestampTzToDateConverter implements Converter<TIMESTAMPTZ, Date> {
INSTANCE;
@Override
public Date convert(TIMESTAMPTZ source) {
//...
}
}
}
JdbcCustomConversions
的构造函数接受 org.springframework.core.convert.converter.Converter
的列表。
转换器应使用 @ReadingConverter
或 @WritingConverter
进行注释,以便控制它们仅适用于读取或写入数据库的适用性。
示例中的 TIMESTAMPTZ
是一种特定于数据库的数据类型,需要转换为更适合域模型的内容。
当您使用Spring Data JDBC提供的 CrudRepository
的标准实现时,它们期望某个表结构。您可以通过在应用程序上下文中提供 NamingStrategy 来进行调整。
当NamingStrategy与数据库表名不匹配时,可以使用 @Table 批注自定义名称。此批注的元素 value
提供自定义表名。以下示例将 MyEntity
类映射到数据库中的 CUSTOM_TABLE_NAME
表:
@Table("CUSTOM_TABLE_NAME")
public class MyEntity {
@Id
Integer id;
String name;
}
当NamingStrategy与数据库列名称不匹配时,您可以使用 @Column 批注自定义名称。此批注的元素 value
提供自定义列名称。以下示例将 MyEntity
类的 name
属性映射到数据库中的 CUSTOM_COLUMN_NAME
列:
public class MyEntity {
@Id
Integer id;
@Column("CUSTOM_COLUMN_NAME")
String name;
}
@Column 注释也可用于引用类型(一对一关系)或集合,列表和映射(一对多关系)在所有这些类型上,注释的 value
元素用于提供引用另一个表中的id列的外键列的自定义名称。在以下示例中, MySubEntity
类的相应表具有名称列,并且出于关系原因, MyEntity
id的id列。此 MySubEntity
类的id列的名称也可以使用 @Column 注释的 value
元素进行自定义:
public class MyEntity {
@Id
Integer id;
@Column("CUSTOM_COLUMN_NAME")
Set<MySubEntity> name;
}
public class MySubEntity {
String name;
}
使用 List
和 Map
时,必须在 List
中为数据集的位置或 Map
中的实体的键值添加一个附加列。可以使用 @Column 注释的 keyColumn
元素自定义此附加列名称:
public class MyEntity {
@Id
Integer id;
@Column(value = "CUSTOM_COLUMN_NAME", keyColumn = "CUSTOM_KEY_COLUMN_NAME")
List<MySubEntity> name;
}
public class MySubEntity {
String name;
}
下表描述了Spring Data JDBC提供的用于检测实体是否为新的策略:
Id-属性检查(缺省值) | 默认情况下,Spring Data JDBC检查给定实体的identifier属性。如果identifier属性为 null ,则假定该实体是新的。否则,假设它不是新的。 |
---|---|
实现 Persistable | 如果实体实现 Persistable ,Spring Data JDBC会将新检测委托给实体的 isNew(…) 方法。有关详细信息,请参阅 Javadoc 。 |
实现 EntityInformation | 您可以通过创建 JdbcRepositoryFactory 的子类并覆盖 getEntityInformation(…) 方法来自定义 SimpleJdbcRepository 实现中使用的 EntityInformation 抽象。然后,您必须将 JdbcRepositoryFactory 的自定义实现注册为Spring bean。请注意,这很少是必要的。有关详细信息,请参阅 Javadoc 。 |
Spring Data JDBC使用ID来标识实体。必须使用Spring Data的 @Id 注释注释实体的ID。
当您的数据库具有ID列的自动增量列时,生成的值在将其插入数据库后在实体中设置。
一个重要的限制是,在保存实体后,该实体不能再是新的。请注意,实体是否是新实体是实体状态的一部分。随着自动增量列,这会自动发生,因为Spring将使用ID列中的值设置ID。如果您没有使用自动增量列,则可以使用 BeforeSave
侦听器,该侦听器设置实体的ID(本文档后面会介绍)。
本节提供有关Spring Data JDBC的实现和使用的一些特定信息。
JDBC模块仅支持将查询手动定义为 @Query
注释中的String。目前不支持从方法名称派生查询。
以下示例显示如何使用 @Query
声明查询方法:
例54.使用@Query声明查询方法
public interface UserRepository extends CrudRepository<User, Long> {
@Query("select firstName, lastName from User u where u.emailAddress = :email")
User findByEmailAddress(@Param("email") String email);
}
Spring完全支持基于
-parameters
编译器标志的Java 8参数名称发现。通过在构建中使用此标志作为调试信息的替代方法,可以省略命名参数的@Param
注释。
Spring Data JDBC仅支持命名参数。
您可以使用 @Query(rowMapperClass = ….)
或通过注册 RowMapperMap
bean并注册 RowMapper
per方法返回类型来配置要使用的 RowMapper
。以下示例显示如何注册 RowMappers
:
@Bean
RowMapperMap rowMappers() {
return new ConfigurableRowMapperMap() //
.register(Person.class, new PersonRowMapper()) //
.register(Address.class, new AddressRowMapper());
}
在确定要用于方法的 RowMapper
时,将根据方法的返回类型执行以下步骤:
RowMapper
。相反,该查询应返回具有单个列的单个行,并且将返回类型的转换应用于该值。
RowMapperMap
中的实体类,直到找到一个有问题的返回类型的超类或接口。使用为该类注册的 RowMapper
。迭代按照注册顺序发生,因此请确保在特定类型之后注册更多通用类型。
如果适用,将打开包装类型(如集合或 Optional
)。因此,返回类型 Optional<Person>
在前面的过程中使用 Person
类型。
您可以使用查询方法 @Modifying
将查询标记为修改查询,如以下示例所示:
@Modifying
@Query("UPDATE DUMMYENTITY SET name = :name WHERE id = :id")
boolean updateName(@Param("id") Long id, @Param("name") String name);
您可以指定以下返回类型:
void
int
(更新记录数)
boolean
(记录是否已更新)
对于 CrudRepository
中的每个操作,Spring Data JDBC都运行多个语句。如果应用程序上下文中存在 SqlSessionFactory ,则Spring Data会针对每个步骤检查 SessionFactory
是否提供语句。如果找到一个,则使用该语句(包括其配置的映射到实体)。
通过将实体类型的完全限定名称与 Mapper.
和确定语句类型的 String
连接来构造语句的名称。例如,如果要插入 org.example.User
的实例,Spring Data JDBC将查找名为 org.example.UserMapper.insert
的语句。
运行该语句时,[ MyBatisContext
]的实例作为参数传递,这使得该语句可以使用各种参数。
下表描述了可用的MyBatis语句:
名称 | 用途 | 可能触发此语句的CrudRepository方法 | 891790 | 中可用的属性 |
---|---|---|---|---|
insert | 插入单个实体。这也适用于聚合根引用的实体。 | save , saveAll 。 | getInstance :要保存的实例 getDomainType :要保存的实体的类型。 get(<key>) :引用实体的ID,其中 <key> 是 NamingStrategy 提供的后引用列的名称。 | |
update | 更新单个实体。这也适用于聚合根引用的实体。 | save , saveAll 。 | getInstance :要保存的实例 getDomainType :要保存的实体的类型。 | |
delete | 删除单个实体。 | delete , deleteById 。 | getId :要删除的实例的ID getDomainType :要删除的实体的类型。 | |
deleteAll-<propertyPath> | 删除使用给定属性路径作为前缀的类型的任何聚合根引用的所有实体。请注意,用于为语句名称添加前缀的类型是聚合根的名称,而不是要删除的实体的名称。 | deleteAll 。 | getDomainType :要删除的实体的类型。 | |
deleteAll | 删除用作前缀 | deleteAll 的类型的所有聚合根。 | getDomainType :要删除的实体的类型。 | |
delete-<propertyPath> | 删除具有给定propertyPath | deleteById 的聚合根引用的所有实体。 | getId :的ID要删除引用实体的聚合根。 getDomainType :要删除的实体的类型。 | |
findById | 按ID | findById 选择聚合根。 | getId :要加载的实体的ID。 getDomainType :要加载的实体的类型。 | |
findAll | 选择所有聚合根 | findAll 。 | getDomainType :要加载的实体的类型。 | |
findAllById | 按ID值 | findAllById 选择一组聚合根。 | getId :要加载的实体的ID值列表。 getDomainType :要加载的实体的类型。 | |
findAllByProperty-<propertyName> | 选择另一个实体引用的一组实体。引用实体的类型用于前缀。引用的实体类型用作后缀。 | 所有 find* 方法。 | getId :引用要加载的实体的实体的ID。 getDomainType :要加载的实体的类型。 | |
count | 计算用作前缀的类型的聚合根数 | count | getDomainType :要计算的聚合根的类型。 |
Spring Data JDBC触发发布到应用程序上下文中任何匹配的 ApplicationListener
的事件。例如,在保存聚合之前调用以下侦听器:
@Bean
public ApplicationListener<BeforeSave> timeStampingSaveTime() {
return event -> {
Object entity = event.getEntity();
if (entity instanceof Category) {
Category category = (Category) entity;
category.timeStamp();
}
};
}
下表描述了可用事件:
事件 | 何时发布 |
---|---|
BeforeDeleteEvent | 在删除聚合根之前。 |
AfterDeleteEvent | 聚合根被删除后。 |
BeforeSaveEvent | 在聚合根保存之前(即插入或更新但在决定是否更新或删除之后)。该事件引用了 AggregateChange 实例。可以通过添加或删除 DbAction 实例来修改实例。 |
AfterSaveEvent | 聚合根保存后(即插入或更新)。 |
AfterLoadEvent | 从数据库 ResultSet 创建聚合根并设置其所有属性后。 |
Spring Data JDBC几乎没有自己的日志记录。相反, JdbcTemplate
发布SQL语句的机制提供了日志记录。因此,如果要检查执行的SQL语句,请激活Spring的 NamedParameterJdbcTemplate 或 MyBatis 的日志记录。
默认情况下,存储库实例上的CRUD方法是事务性的。对于读取操作,事务配置 readOnly
标志设置为 true
。所有其他配置都使用普通的 @Transactional
注释,以便应用默认事务配置。有关详细信息,请参阅 SimpleJdbcRepository 的Javadoc。如果需要为存储库中声明的方法之一调整事务配置,请重新声明存储库接口中的方法,如下所示:
例55. CRUD的自定义事务配置
public interface UserRepository extends CrudRepository<User, Long> {
@Override
@Transactional(timeout = 10)
public List<User> findAll();
// Further query method declarations
}
前面的内容导致 findAll()
方法执行超时10秒且没有 readOnly
标志。
更改事务行为的另一种方法是使用通常涵盖多个存储库的Facade或服务实现。其目的是为非CRUD操作定义事务边界。以下示例显示如何创建此类Facade:
示例56.使用Facade定义多个存储库调用的事务
@Service
class UserManagementImpl implements UserManagement {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
@Autowired
public UserManagementImpl(UserRepository userRepository,
RoleRepository roleRepository) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
}
@Transactional
public void addRoleToAllUsers(String roleName) {
Role role = roleRepository.findByName(roleName);
for (User user : userRepository.findAll()) {
user.addRole(role);
userRepository.save(user);
}
}
前面的示例导致调用 addRoleToAllUsers(…)
在事务中运行(参与现有事务或创建新事务(如果没有已运行))。由于外部事务配置确定要使用的实际存储库,因此忽略了存储库的事务配置。请注意,您必须显式激活 <tx:annotation-driven />
或使用 @EnableTransactionManagement
来获取外观工作的基于注释的配置。请注意,前面的示例假定您使用组件扫描。
要让查询方法成为事务性的,请在您定义的存储库接口中使用 @Transactional
,如以下示例所示:
例57.在查询方法中使用@Transactional
@Transactional(readOnly = true)
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findByLastname(String lastname);
@Modifying
@Transactional
@Query("delete from User u where u.active = false")
void deleteInactiveUsers();
}
通常,您希望将 readOnly
标志设置为true,因为大多数查询方法只读取数据。与此相反, deleteInactiveUsers()
使用 @Modifying
注释并覆盖事务配置。因此,该方法将 readOnly
标志设置为 false
。
将事务用于只读查询是绝对合理的,我们可以通过设置
readOnly
标志来标记它们。但是,这不会检查您是否触发了操作查询(尽管某些数据库会拒绝只读事务中的INSERT
和UPDATE
语句)。相反,readOnly
标志作为提示传播到底层JDBC性能优化的驱动程序。
Spring Data提供了复杂的支持,可以透明地跟踪创建或更改实体的人员以及更改发生的时间。要从该功能中受益,您必须为您的实体类配备审计元数据,该元数据可以使用注释或通过实现接口来定义。
我们提供 @CreatedBy
和 @LastModifiedBy
来捕获创建或修改实体的用户以及 @CreatedDate
和 @LastModifiedDate
以在发生更改时捕获。
例子58.一个被审计的实体
class Customer {
@CreatedBy
private User user;
@CreatedDate
private DateTime createdDate;
// … further properties omitted
}
如您所见,可以有选择地应用注释,具体取决于您要捕获的信息。捕获何时进行更改的注释可用于Joda-Time, DateTime
,遗留Java Date
和 Calendar
,JDK8日期和时间类型以及 long
或 Long
类型的属性。
如果您不想使用注释来定义审核元数据,可以让您的域类实现 Auditable
接口。它公开了所有审计属性的setter方法。
还有一个便利基类 AbstractAuditable
,您可以扩展它以避免需要手动实现接口方法。这样做会增加域类与Spring Data的耦合,这可能是您想要避免的。通常,基于注释的定义审计元数据的方式是优选的,因为它具有较小的侵入性和更灵活性。
如果您使用 @CreatedBy
或 @LastModifiedBy
,审计基础架构需要以某种方式了解当前主体。为此,我们提供了一个 AuditorAware<T>
SPI接口,您必须实现该接口以告知基础架构当前用户或系统与应用程序交互的人员。泛型类型 T
定义了使用 @CreatedBy
或 @LastModifiedBy
注释的属性的类型。
以下示例显示了使用Spring Security的 Authentication
对象的接口的实现:
例59.基于Spring Security的AuditorAware的实现
class SpringSecurityAuditorAware implements AuditorAware<User> {
public Optional<User> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}
该实现访问Spring Security提供的 Authentication
对象,并查找您在 UserDetailsService
实现中创建的自定义 UserDetails
实例。我们在此假设您通过 UserDetails
实现公开域用户,但是根据找到的 Authentication
,您也可以从任何地方查找它。
要激活审核,请将 @EnableJdbcAuditing
添加到您的配置中,如以下示例所示:
示例60.使用Java配置激活审计
@Configuration
@EnableJdbcAuditing
class Config {
@Bean
public AuditorAware<AuditableUser> auditorProvider() {
return new AuditorAwareImpl();
}
}
如果将 AuditorAware
类型的bean公开给 ApplicationContext
,则审计基础结构会自动选择它并使用它来确定要在域类型上设置的当前用户。如果在 ApplicationContext
中注册了多个实现,则可以通过显式设置 @EnableJdbcAuditing
的 auditorAwareRef
属性来选择要使用的实现。
抱歉。到目前为止,我们没有经常被问到的问题
AOP
CRUD
依赖注入
JPA
Spring
<repositories />
元素触发Spring Data存储库基础结构的设置。最重要的属性是 base-package
,它定义了扫描Spring Data存储库接口的包。见“ XML configuration ”。下表描述了 <repositories />
元素的属性:
姓名 | 说明 |
---|---|
base-package | 定义要扫描的存储库接口的包,该存储库接口在自动检测模式下扩展 *Repository (实际接口由特定的Spring数据模块确定)。也会扫描配置包下面的所有包。允许使用通配符。 |
repository-impl-postfix | 定义后缀以自动检测自定义存储库实现。名称以配置的后缀结尾的类被视为候选。默认为 Impl 。 |
query-lookup-strategy | 确定用于创建查找程序查询的策略。有关详细信息,请参阅“ Query Lookup Strategies ”。默认为 create-if-not-found 。 |
named-queries-location | 定义搜索包含外部定义查询的属性文件的位置。 |
consider-nested-repositories | 是否嵌套存储库接口应该考虑定义。默认为 false 。 |
<populator />
元素允许通过Spring Data存储库基础结构填充数据存储。[1]
姓名 | 说明 |
---|---|
locations | 应在哪里找到要从存储库中读取对象的文件。 |
Spring Data JDBC尚不支持查询派生。
下表列出了Spring Data存储库通常支持的返回类型。但是,请查阅特定于 Store 的文档以获取支持的返回类型的确切列表,因为此处列出的某些类型可能在特定 Store 中不受支持。
地理空间类型(例如
GeoResult
,GeoResults
和GeoPage
)仅适用于支持地理空间查询的数据存储。
返回类型 | 说明 |
---|---|
void | 表示没有返回值。 |
Primitives | Java原语。 |
包装器类型 | Java包装器类型。 |
T | 一个独特的实体。期望查询方法最多返回一个结果。如果未找到结果,则返回 null 。多个结果会触发 IncorrectResultSizeDataAccessException 。 |
Iterator<T> | Iterator 。 |
Collection<T> | A Collection 。 |
List<T> | A List 。 |
Optional<T> | Java 8或Guava Optional 。期望查询方法最多返回一个结果。如果未找到结果,则返回 Optional.empty() 或 Optional.absent() 。多个结果会触发 IncorrectResultSizeDataAccessException 。 |
Option<T> | Scala或Vavr Option 类型。语义上与前面描述的Java 8的 Optional 相同。 |
Stream<T> | Java 8 Stream 。 |
Streamable<T> | Iterable 的便利扩展,直接将方法暴露给流,映射和过滤结果,连接它们等. |
实现 Streamable 并采用 Streamable 构造函数或工厂方法参数的类型 | 使用 Streamable 作为参数公开构造函数或 ….of(…) / ….valueOf(…) 工厂方法的类型。有关详细信息,请参阅 Returning Custom Streamable Wrapper Types 。 |
Vavr Seq , List , Map , Set | Vavr集合类型。有关详细信息,请参阅 Support for Vavr Collections 。 |
Future<T> | A Future 。期望使用 @Async 注释的方法,并且需要启用Spring的异步方法执行功能。 |
CompletableFuture<T> | Java 8 CompletableFuture 。期望使用 @Async 注释的方法,并且需要启用Spring的异步方法执行功能。 |
ListenableFuture | A org.springframework.util.concurrent.ListenableFuture 。期望使用 @Async 注释的方法,并且需要启用Spring的异步方法执行功能。 |
Slice | 一大块数据,表明是否有更多可用数据。需要 Pageable 方法参数。 |
Page<T> | A Slice 包含其他信息,例如结果总数。需要 Pageable 方法参数。 |
GeoResult<T> | 包含附加信息的结果条目,例如到参考位置的距离。 |
GeoResults<T> | 包含附加信息的 GeoResult<T> 列表,例如到参考位置的平均距离。 |
GeoPage<T> | 带有 GeoResult<T> 的 Page ,例如到参考位置的平均距离。 |
Mono<T> | 项目反应堆 Mono 使用被动存储库发出零个或一个元素。期望查询方法最多返回一个结果。如果未找到结果,则返回 Mono.empty() 。多个结果会触发 IncorrectResultSizeDataAccessException 。 |
Flux<T> | 项目反应堆 Flux 使用被动存储库发出零个,一个或多个元素。返回 Flux 的查询也可以发出无限数量的元素。 |
Single<T> | 使用被动存储库发出单个元素的RxJava Single 。期望查询方法最多返回一个结果。如果未找到结果,则返回 Mono.empty() 。多个结果会触发 IncorrectResultSizeDataAccessException 。 |
Maybe<T> | 一个RxJava Maybe 使用被动存储库发出零个或一个元素。期望查询方法最多返回一个结果。如果未找到结果,则返回 Mono.empty() 。多个结果会触发 IncorrectResultSizeDataAccessException 。 |
Flowable<T> | 一个RxJava Flowable 使用被动存储库发出零个,一个或多个元素。查询返回 Flowable 也可以发出无限数量的元素。 |