3.1. 核心概念
CrudRepository包含增删查改基础功能
public interface CrudRepository<T, ID extends Serializable>
extends Repository<T, ID> {
<S extends T> S save(S entity); //保存给定实体
Optional<T> findById(ID primaryKey); //根据给定主键id查找实体
Iterable<T> findAll(); //返回所有实体
long count(); //返回实体数量
void delete(T entity); //删除给定实体
boolean existsById(ID primaryKey); //根据给定实体判断一个实体是否存在
// … more functionality omitted.
}
PagingAndSortingRepository 继承自 CrudRepository并提供分页和排序功能
public interface PagingAndSortingRepository<T, ID extends Serializable>
extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
如果要在以20为一页的结果中,获取第2页结果,则如下使用:
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(new PageRequest(1, 20));
除了根据方法查询,还可以查询数量和删除。
interface UserRepository extends CrudRepository<User, Long> {
//根据 lastname来得到数量,相当于 select count(*) from table t where t.lastname= lastname
long countByLastname(String lastname);
}
interface UserRepository extends CrudRepository<User, Long> {
//根据lastname来删除,返回删除的数量,类似于 delete from table t where t.lastname = lastname
long deleteByLastname(String lastname);
//根据lastname来删除,返回被删除的实体集合
List<User> removeByLastname(String lastname);
}
3.2 方法查询
步骤:
- 声明一个接口,继承 Repository 或它的一个子类,你要加上对应的实体类和对应的主键、
interface PersonRepository extends Repository<Person, Long> { … }
- 在接口中定义方法查询
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByLastname(String lastname);
}
- 创建接口的代理实例来让Spring管理,可以通过 Java方式来配置
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories
class Config {}
或通过 xml 来配置
<?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的命名空间需要你自己作对应的修改。
- 注入 repository 实例并使用
class SomeClient {
private final PersonRepository repository;
SomeClient(PersonRepository repository) {
this.repository = repository;
}
void doSomething() {
List<Person> persons = repository.findByLastname("Matthews");
}
}
下面的部分将详细说明每一步。
3.3 定义repository接口
定义实体类的repository接口,必须继承Repository并加上相应的实体类和ID类型,如果你实体想获得 CRUD 方法,那就继承 CrudRepository
,而不是 Repository
3.3.1
通常,你的 repository接口会继承 Repository
,CrudRepository
或 PagingAndSortingRepository
。当然了,如果你不想继承这些Spring Data 内置的接口,你可以通过在repository上使用 @RepositoryDefinition注解。继承 CrudRepository 可以暴露一些操作实体的方法,你可以从 CrudRepository 中将你想要的功能复制进你的 repository中。
这将使你能定义自己的顶层 Spring Data Repository。
选择性地暴露CRUD方法
@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> 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);
}
包配置自动
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
interface Configuration { }
3.4 查询方法
repository可以通过方法名(这个好哇~)或手动查询(@Query注解)来执行查询
3.4.1
interface PersonRepository extends Repository<User, 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);
// 所有属性忽略大小写
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// 查询中使用 Order By
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
- 在表达式中还可以使用
AND
OR
Between
LessThan
GreaterThan
Like
等,请参阅手册 - IgnoreCase 单个属性忽略大小写,AllIgnoreCase 所有属性忽略大小写
- OrderBy 排序 Asc 或 Desc(降序)
3.4.3 属性表达式
如果 Person 中的 Address属性有 ZipCode属性,则可以如下:
List<Person> findByAddressZipCode(ZipCode zipCode);
会解析为 x.address.zipCode
,算法从右边开始检测,会先将它分为 AddressZip
和 Code
,但是匹配失败,于是分为 Address
和 ZipCode
。
在第一次匹配成功的话,便会忽略下一个可能的匹配,假如 Person 还有一个属性 为 addressZip
, 而算法检测出 addressZip 无 Code 属性,于是报错。
为了避免混淆,可以使用 _
来分开它们。
List<Person> findByAddress_ZipCode(ZipCode zipCode);
虽然这也能解析正确,但是我们最好还是按Java命名规范来命名属性(别使用_,而使用驼峰记法)
3.4.4 特殊的处理参数
Example 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);
Page包今所有元素的数量和页,它通过触发查询数量来实现,所以这有可能会花费很长时间,取决于存储的数据量。而Slice就不一样了,它只知道是否有下一个Slice可用,在大量数据集中这很实用。
3.4.5 限制查询结果
查询方法的结果可以通过关键字first或top进行限制,可以互换使用。 可以在
first/top 后附加到指定要返回的最大结果大小。 如果该数字被遗漏,则假设结果大小为1。
Example 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
。
如果分页或切片(Slice)应用于限制查询分页(以及可用页数的计算),则在有限的结果内应用。
请注意,通过Sort参数将结果与动态排序结合使用,可以表达'K'最小以及'K'最大元素的查询方法。
3.4.6 流式查询结果
可以通过使用Java 8 Stream <T>作为返回类型来逐步处理查询方法的结果。 不是将查询结果简单地包含在Stream数据存储中,而是使用特定的方法来执行流。
示例16.使用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()方法或使用Java 7 try-with-resources块手动关闭Stream。
示例17. 在一个try-with-resources块中使用Stream <T>
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
并不是所有的Spring数据模块目前都支持Stream <T>作为返回类型。
3.4.7 异步查询结果####
可以使用Spring的异步方法执行功能异步执行存储库(respository)查询。 这意味着方法将在调用时立即返回,并且实际的查询执行将发生在已提交给Spring TaskExecutor的任务中。
//java.util.concurrent.Future作为返回类型
@Async
Future<User> findByFirstname(String firstname);
//返回类型为Java 8的 java.util.concurrent.CompletableFuture
@Async
CompletableFuture<User> findOneByFirstname(String firstname);
//返回类型为org.springframework.util.concurrent.ListenableFuture
@Async
ListenableFuture<User> findOneByLastname(String lastname);
3.5 创建存储库(respository)实例
在本节中,您将为定义的存储库接口创建实例和bean定义。 一种方法是使用支持存储库机制的每个Spring数据模块附带的Spring命名空间,尽管我们通常建议使用Java-Config风格配置。
3.5.1 XML 配置
每个Spring Data模块都包含一个存储库元素,它允许您简单地定义Spring为您扫描的基础包。
示例18.通过XML启用Spring数据存储库
<?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属性允许通配符,以便您可以定义扫描包的模式。
使用过滤器
默认情况下,框架会自动检查每个接口,并扩展位于配置的基础包下方的持久化特定的Repository子接口,并为其创建一个bean实例。 但是,您可能需要对要为其创建的接口bean实例进行更细粒度的控制。 为此,您可以在<repositories />中使用<include-filter />和<exclude-filter />元素。 语义与Spring的上下文命名空间中的元素完全相同。 有关详细信息,请参阅 Spring参考文档。
例如,要将某些接口从存储库实例化中排除,可以使用以下配置:
示例19.使用exclude-filter元素
<repositories base-package="com.acme.repositories">
<context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>
此示例将以实例化方式排除以SomeRepository结尾的所有接口。
3.5.2 Java配置
也可以在Java配置类上使用特定于存储的@ Enable $ {store}Repositories 注解触发 repository 架构。 有关Spring容器的基于Java的配置的介绍,请参阅参考文档。1
启用Spring Data repository 的示例配置看起来像这样。
示例20.基于注解的存储库配置例子
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
该示例使用JPA特定注释,您可以根据实际使用的 repository 模块进行更改。 这同样适用于EntityManagerFactory bean 的定义。 请参阅涵盖 存储 配置的部分。
3.5.3 单独使用
您还可以使用Spring容器外的存储库基础结构,例如 在CDI环境中。 您在类路径中仍然需要一些Spring库,但通常可以通过编程方式设置存储库。 提供存储库支持的Spring数据模块提供了一个持久化特定的RepositoryFactory,可以如下所示来使用RepositoryFactory。
示例21.存储库工厂的独立使用
RepositoryFactorySupport factory = … // 实例化 factory
UserRepository repository = factory.getRepository(UserRepository.class);
3.6 Spring Data Repository 的自定义实现
在本节中,您将了解自定义Repository以及如何形成复合存储库。
当查询方法需要不同的行为或不能通过查询推导实现,很有必要地提供自定义实现。 Spring Data Repository 可以轻松地允许您提供自定义 Repository 代码,并将其与通用CRUD抽象和查询方法功能集成。
3.6.1 定制 Repository
为了丰富具有自定义功能的 Repository,您首先要为自定义功能定义一个片段接口和一个实现。 然后让您的 Repository 接口从该片段接口扩展。
示例22.自定义Repository接口方法
interface CustomizedUserRepository {
void someCustomMethod(User user);
}
示例23.实现自定义的 Repository 接口方法
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
public void someCustomMethod(User user) {
// Your custom implementation
}
}
该类的最重要的点是与片段接口相比的名称多了Impl后缀。
示例24.更改您的 Repository 接口
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {
// Declare query methods here
}
让您的 Repository 接口继承多一个接口。 这样做结合了CRUD和自定义功能,并将其提供给客户端。
Spring Data 提供了很多 Repository ,都可以互相组合来增强你的接口。 基础 Repository 提代了很多的方法,再加上你自定义的方法,那会使你的自定义接口更强大。 每次向Repository接口添加新接口时,都可以增强你的组合。 基础 Repository 接口和相关实现由相应的 Spring Data 模块提供。(也就是通过继承Spring Data提供的一些Repository接口,可以组成强大的Repository接口,而且Spring Data提代的Repository接口不用你再去实现,并且很多接口里面已有很多好用的方法 )
示例25.实现类片段
interface HumanRepository {
void someHumanMethod(User user);
}
class HumanRepositoryImpl implements HumanRepository {
public void someHumanMethod(User user) {
// Your custom implementation
}
}
interface EmployeeRepository {
void someEmployeeMethod(User user);
User anotherEmployeeMethod(User user);
}
class ContactRepositoryImpl implements ContactRepository {
public void someContactMethod(User user) {
// Your custom implementation
}
public User anotherContactMethod(User user) {
// Your custom implementation
}
}
示例26.更改你的Repository 接口
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {
// Declare query methods here
}
存储库可以由根据其声明的顺序导入的多个自定义实现构成。 自定义实现具有比基本实现和存储库方面更高的优先级。 这个顺序允许您覆盖基本存储库和方面方法,并且如果两个片段贡献相同的方法签名,则可以解析歧义。 存储库片段不限于在单个存储库界面中使用。 多个存储库可以使用片段接口在不同的存储库之间重用定制。
示例27 覆盖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
}
}
示例28.定制的存储库接口
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}
interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
- 配置
如果使用命名空间配置,repository 基础框架会尝试通过扫描我们找到存储库的包下面的类来自动检测自定义实现片段。这些类需要遵循将命名空间元素的属性repository-impl-postfix附加到找到的命名约定 片段接口名称。 此后缀默认为Impl。
示例29.配置示例
<repositories base-package="com.acme.repository" />
<repositories base-package="com.acme.repository" repository-impl-postfix="FooBar" />
第一个配置示例将尝试查找一个类com.acme.repository.CustomizedUserRepositoryImpl作为自定义存储库实现,而第二个示例将尝试查找com.acme.repository.CustomizedUserRepositoryFooBar。
- 解决歧义
如果在不同的包中找到具有匹配类名的多个实现,Spring Data将使用bean名称来标识哪个才是正确的。
给定上面介绍的CustomizedUserRepository的以下两个自定义实现,第一个实现将被选中。 它的bean名称是customUserRepositoryImpl匹配片段接口(CustomizedUserRepository)加上后缀Impl。
示例30.解决实现类歧义
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
}
如果您在 UserRepository 接口上使用 @Component(“specialCustom”)
注解,则 bean 名称加上Impl匹配为 com.acme.impl.two
中的存储库实现定义,它将被选择而不是第一个。(有注解就按注解的名来匹配~)
- 手动关联
如果您的自定义实现仅使用基于注释的配置和自动布线,那么刚才显示的方法效果很好,因为它将被视为任何其他Spring bean。 如果您的实现片段bean需要特殊布线,则只需声明bean并按照刚才描述的约定命名该bean。 然后,底层将通过名称引用手动定义的bean,而不会自动去创建它。
实例31.自定义实现的手动关联
<repositories base-package="com.acme.repository" />
<beans:bean id="userRepositoryImpl" class="…">
<!-- further configuration -->
</beans:bean>
3.6.2 自定义 Base Repository
当您要自定义 Base Repository 的方法时,需要自定义所有存储库接口,因此所有存储库都会受到影响。 要更改所有存储库的行为,您需要创建一个基于特定实体类的存储库基类的实现。 然后,此类将用作存储库代理的自定义基类。
示例32. 自定义 repository 基类
class MyRepositoryImpl<T, ID extends Serializable>
extends SimpleJpaRepository<T, ID> {
private final EntityManager entityManager;
MyRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
//使 EntityManager 可以在新引入的方法中使用。
this.entityManager = entityManager;
}
@Transactional
public <S extends T> S save(S entity) {
// implementation goes here
}
}
该类需要具有专门的存储库工厂实现使用的超类的构造函数。 一般来说存储库基类具有多个构造函数,只要覆盖其中使用了EntityInformation和底层对象(例如,EntityManager或模板类)的构造函数即可。
最后一步是使Spring Data 架构识别到你定制的基类。 在JavaConfig中,这是通过使用 @ Enable ... Repositories
注解的 repositoryBaseClass
属性实现的:
示例33.使用JavaConfig配置自定义存储库基类
@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
相应的属性在XML命名空间中可用。
示例34.使用XML配置自定义存储库基类
<repositories base-package="com.acme.repository"
base-class="….MyRepositoryImpl" />
3.7 从 aggregate roots 建立事件
存储库管理的实体是聚合根(aggregate roots)。 在 Domain 驱动设计应用程序中,这些聚合根通常会发布域事件。 Spring Data 提供了一个注释 @DomainEvents
,您可以在聚合根的方法上加上它来使该发布尽可能简单。
示例35.从聚合根中暴露域事件
class AnAggregateRoot {
@DomainEvents ❶
Collection<Object> domainEvents() {
// … return events you want to get published here
}
@AfterDomainEventPublication ❷
void callbackMethod() {
// … potentially clean up domain events list
}
}
❶ 使用@DomainEvents的方法可以返回单个事件实例或事件集合。 它不能采取任何论据。
❷在所有事件发布之后,使用 @AfterDomainEventPublication
注释的方法。 可用于清理要发布的事件列表。
每当调用一个 Spring data repository 的save(...)方法时,都会调用这些方法。
3.8 空安全
存储库方法可以提高空安全性,以便在编译时处理空值,而不是在运行时碰到 NullPointerException
。 这使您的应用程序通过清理可空性声明更安全,表达“值或无价值”语义,而无需使用如 Optional
之类的包装器。
您可以使用Spring Framework的注释来表达空安全的存储库方法。 它们提供了一个工具友好的方法,并在运行期间选择进行空检查:
- @NonNullApi 注解,在包级别将非null作为默认行为声明
- @Nullable 注解,其中特定参数或返回值可以为null。
两个注释都使用JSR-305元注释进行元注释(一种休眠的JSR,但由像IDEA,Eclipse,Findbugs等工具所支持)可以为Java开发人员提供有用的警告。
如果您打算使用自己的元注释,请确保在类路径中包含包含JSR-305的@Nonnull注释的JAR文件。
在程序包级或Kotlin中声明的空声明范围内的存储库查询方法的调用在运行时都会被验证。 将null值传递给不可为空的查询方法参数被异常拒绝。 不产生结果且不可为空的查询方法将抛出 EmptyResultDataAccessException 而不是返回null。
示例36.激活包的非空默认值
@org.springframework.lang.NonNullApi
package com.example;
示例37.声明参数和返回值的可空性
package com.example; ❶
interface UserRepository extends Repository<User, String> {
List<User> findByLastname(@Nullable String firstname); ❷
@Nullable
User findByFirstnameAndLastname(String firstname, String lastname); ❸
}
❶ @NonNullApi在包级别上声明此包中的所有API默认为非空。
❷ @Nullable允许在特定参数上使用空值。 每个可空参数都必须注释。
❸ 可能返回null的方法用@Nullable注释。
如果您使用Kotlin声明存储库接口,则可以使用Kotlin的空安全来表示可空性。
示例38.声明Kotlin中参数和返回值的可空性
interface UserRepository : Repository<User, String> {
fun findByLastname(username: String?): List<User>
fun findByFirstnameAndLastname(firstname: String, lastname: String): User?
}
Kotlin代码对字节码进行编译,该字节码不能使用方法签名表示可空性声明,而不是对元数据编译。 确保包含kotlin反射,以便启用Kotlin的可空性声明。
3.9 Spring Data 扩展
本节介绍了一组Spring数据扩展,可在各种上下文中启用Spring数据使用。 目前大部分的集成针对Spring MVC。
3.9.1 Querydsl扩展
Querydsl是一个框架,可以通过流畅的API构建静态类型的类SQL查询。
有若干个Spring数据模块通过QueryDslPredicateExecutor提供与Querydsl的集成。
示例39. QueryDslPredicateExecutor接口
public interface QueryDslPredicateExecutor<T> {
Optional<T> findById(Predicate predicate); ❶
Iterable<T> findAll(Predicate predicate); ❷
long count(Predicate predicate); ❸
boolean exists(Predicate predicate); ❹
// … more functionality omitted.
}
❶ 查找并返回与 predicate 匹配的单个实体。
❷ 查找并返回与 predicate 匹配的所有实体。
❸ 返回与 predicate 匹配的实体数。
❹ 如果与谓词匹配的实体存在,则返回。
要使用Querydsl支持,只需在您的存储库接口上扩展QueryDslPredicateExecutor。
示例40.在存储库上进行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);