Spring Data JPA 为 Java持久化API 数据库(JPA)提供了存储库支持,减少了实现数持久化访问层所需的样板代码量,它简化了需要访问数据源的应用程序的开发。
1.引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependencies>
2.核心概念
Spring Data 存储库的抽象接口是 Repository。它使用实体类以及实体类的 ID 类型作为类型参数。此接口主要用于捕获要使用的实体类型,并帮助您扩展接口。如 CrudRepository 为正在管理的实体类提供了复杂的 CRUD 功能。
CrudRepository 接口
public interface CrudRepository<T, ID> 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); //指示具有给定 ID 的实体是否存在
// … more functionality omitted.
}
在 CrudRepository之上,有一个 PagingAndSortingRepository抽象,它增加了额外的方法来简化对实体的分页访问:
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));
除了查询方法之外,还有计数查询和删除查询。下面显示了计数查询的接口定义:
计数查询
interface UserRepository extends CrudRepository<User, Long> {
long countByLastname(String lastname);
}
下面显示了删除查询的接口定义:
删除查询
interface UserRepository extends CrudRepository<User, Long> {
long deleteByLastname(String lastname);
List<User> removeByLastname(String lastname);
}
3.查询方法
标准CRUD功能存储库通常在基础数据存储上进行查询。使用 Spring Data,声明这些查询将使用三个过程:
- 声明一个继承 Repository 或其子接口的接口,并添加它应该处理的实体类和 ID 类型,如下面的示例所示:
interface PersonRepository extends Repository<Person, Long> { … }
- 在接口上声明查询方法。
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByLastname(String lastname);
}
- 注入存储库实例并使用它,如下面的示例所示:
class SomeClient {
@autowired
private PersonRepository repository;
void doSomething() {
List<Person> persons = repository.findByLastname("Matthews");
}
}
下面详细讲解了每个步骤
3.1 定义存储库接口
首先,定义实体类特定的存储库接口。该接口必须继承 Repository 并且类型指定为实体类和实体类的 ID 类型。如果希望公开该实体类的 CRUD 方法,请继承 CrudRepository 而不是 Repository。
通常,您的存储库接口扩展 Repository、 CrudRepository 或 PagingAndSortingRepository。如果不想扩展 Spring Data 接口,也可以使用 @repositorydefinition 注释存储库接口。
下面的示例演示如何有选择地公开 CRUD 方法(在本例中为 findById 和 save) :
选择显示 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 (...)。因此,UserRepository 现在可以保存用户,通过 ID 查找单个用户,并触发一个查询,通过电子邮件地址查找用户。
3.2 定义查询方法
存储库代理有两种从方法名派生查询的方式:
- 通过直接从方法名派生查询。
- 通过使用手动定义的查询。
查询创建
Spring Data repository 内置查询构建器机制,该机制解析方法的前缀,如 find... By,read... By,query... By,count... By,和get... By 。这些方法可以包含其他表达式,例如在要创建的查询上设置 Distinct 标志。第一个 By 用作分隔符,表示条件的开始,后面定义实体属性的各种条件,并将它们用 And 和 Or 连接起来。下面的示例演示如何创建大量查询:
从方法名创建查询
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// 为查询启用 distinct 标志
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 标志(例如,
findByLastnameIgnoreCase (...)),或支持忽略大小写的类型的所有属性(通常是 String 实例),例如findByLastnameAndFirstnameAllIgnoreCase (...)。是否支持忽略大小写可能因存储类型而异,因此请参考参考文档中关于特定存储的查询方法部分。 - 您可以将
OrderBy子句附加到查询方法并提供排序方向(Asc或Desc)来实现静态排序。若要创建支持动态排序的查询方法,请参阅“特殊参数处理”。
属性表达式
属性表达式只能引用实体的直接属性。在创建查询时,请确保解析的属性是实体类的属性。也可以通过遍历嵌套属性来定义约束。考虑下面的方法签名:
List<Person> findByAddressZipCode(ZipCode zipCode);
假设 Person 的 Address 有 ZipCode 属性。在这种情况下,该方法将创建属性遍历 x.address.zipCode。解析算法首先将整个部分(AddressZipCode)解释为属性,并检查具有该名称的属性的实体类(非大写)。如果解析成功,它将使用该属性。如果没有,算法会将驼峰式变量从右侧分割为头部和尾部,并尝试找到相应的属性ーー如示例中会被分为 AddressZip 和 Code。算法继续分割头部,找到属性,它就会取下尾部,然后继续从那里构建树,按照刚才描述的方式将尾部向上分割。如果这种拆分不匹配,则算法将拆分点移到左侧(Address,ZipCode)并继续执行如上操作。
虽然这在大多数情况下都可以工作,但是算法可能会选择错误的属性。假设 Person 类也有一个 addressZip 属性。算法将在第一轮分割中匹配,选择错误的属性,并失败(因为 addressZip 的类型可能没有代码属性)。
要解决这种不确定性,可以在方法名称中使用 _ 来手动定义遍历点。所以我们的方法名如下:
List<Person> findByAddress_ZipCode(ZipCode zipCode);
因为我们将下划线字符视为保留字符,所以我们强烈建议遵循标准的 Java 命名约定(也就是说,在属性名称中不使用下划线,而是使用驼峰式大小写)。
特殊参数处理
若要处理查询中的参数,请参照前面的示例中那样定义方法参数。除此之外,查询方法还可以识别某些特定类型,如 pagable 和 Sort,可以使用他们对查询应用动态地分页和排序。下面的例子展示了这些特性:
在查询方法使用 Pageable 、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);
如果您不想应用任何排序或分页,请使用
Sort.unsorted()及Pageable.unpaged()填充方法。
第一个方法允许传递 org.springframework.data.domain.Pageable 实例到查询方法,动态地向静态查询添加分页。Page 对象携带记录条数以及页数。它通过触发一个计数查询来计算总数。
排序选项也是通过 Pageable 实例处理的。如果只需要排序,则添加 org.springframework.data.domain.Sort 参数到方法中,返回 List 类型。
分页和排序
可以使用属性名定义简单排序表达式。可以将表达式连接起来,以便将多个条件收集到一个表达式中。
定义排序表达式
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
定义排序表达式类型安全性更高的方法:从定义排序表达式的类型开始,并使用方法引用定义要进行排序的属性。
使用类型安全 API 定义排序表达式
TypedSort<Person> person = Sort.sort(Person.class);
Sort sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
使用运行时代理通常使用 CGlib,当使用 Graal VM Native 这样的工具时,它可能会干扰本机映像编译
如果你的存储实现支持 Querydsl,你也可以使用生成的元模型来定义排序表达式
使用 Querydsl API 定义排序表达式
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
Spring Data Web 支持
HandlerMethodArgumentResolvers 实现可以让 Spring MVC 从请求参数中解析 pagable 和 Sort 实例。
使用 pagable 作为控制器方法参数
@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 尝试通过使用以下缺省配置从请求参数派生一个可分页的实例:
| 请求参数 | ||
|---|---|---|
page | 要检索的页面。0索引,默认值为0。 | |
size | 要检索的页面的大小。默认值为20。 | |
| `sort | 应该以下面的格式 `property,property (,ASC | DESC)(,IgnoreCase)进行排序。默认排序方向是区分大小写的升序。如果你想改变排序方向或设置忽略大小写,可以使用多个排序参数ーー例如,?sort=firstname&sort=lastname,asc&sort=city,ignorecase` |
如果需要从请求中解析多个 pagable 或 Sort 实例(例如,对于多个表) ,可以使用 Spring 的@qualifier 注释来区分不同的实例。然后,请求参数必须以 ${ qualifier } _ 为前缀。下面的例子显示了产生的方法签名:
String showUsers(Model model,
@Qualifier("thing1") Pageable first,
@Qualifier("thing2") Pageable second) { … }
你必须填充 thing1_page 和thing2_page
传递到该方法的默认 pagable 相当于 PageRequest.of (0,20) ,但可以通过使用 pagable 参数上的@pageabledefault 注释进行自定义。
限制查询结果
查询方法的结果可以通过使用前两个关键字来限制,这两个关键字可以互换使用。可以将一个可选的数值附加到 top 或 first 以指定要返回的最大结果大小。如果省略该数字,则假定结果大小为1。下面的示例演示如何限制查询大小:
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 关键字中。
Repository 方法的空处理
在 Spring Data 2.0中,返回单个聚合实例的存储库 CRUD 方法使用 java8的 Optional 来表示潜在的缺失值。除此之外,Spring Data 支持在查询方法中返回以下包装类型:
com.google.common.base.Optionalscala.Optionio.vavr.control.Option
或者,查询方法可以选择根本不使用包装类型。查询结果的缺失将通过返回 null 来表示。保证返回集合、集合替代项、包装器和流的存储库方法永远不会返回 null,而是返回相应的空表示。详细信息请参阅“ Repository query return types”。
-----------------------------------待补充
流式查询结果
查询方法的结果可以通过使用 java8 stream < t > 作为返回类型来逐步处理。使用特定于 Stream 数据存储的方法来执行流,而不是将查询结果包装在 Stream 数据存储中,如下面的示例所示:
使用 java8对查询结果进行流处理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()关闭,或使用 java7 try-with-resources 块,如下面的例子所示:
使用 try-with-resources块
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
并非所有 Spring Data 模块目前都支持
Stream<T>作为返回类型。
异步查询结果
可以使用 Spring 的异步方法执行功能异步运行存储库查询。这意味着方法在调用时立即返回,而实际的查询执行发生在已提交给 Spring TaskExecutor 的任务中。异步查询执行不同于反应式查询执行,不应该混合使用。有关反应式支持的更多详细信息,请参阅特定的存储文档。下面的示例显示了一些异步查询:
@Async
Future<User> findByFirstname(String firstname);
//java.util.concurrent.Future
@Async
CompletableFuture<User> findOneByFirstname(String firstname); //java8 java.util.concurrent.CompletableFuture
@Async
ListenableFuture<User> findOneByLastname(String lastname);
//org.springframework.util.concurrent.ListenableFuture
使用@Query注解定义查询
在查询方法上声明查询@Query
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
}
使用高级 LIKE 表达式
在查询定义中定义 LIKE 表达式,如下面的示例所示:
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.firstname like %?1")
List<User> findByFirstnameEndsWith(String firstname);
}
Native Queries
@ query 注释允许通过将 nativeQuery 标志设置为 true 来运行本地查询,如下面的示例所示:
public interface UserRepository extends JpaRepository<User, Long> {
@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
User findByEmailAddress(String emailAddress);
}
在查询方法中声明本机计数查询的分页@Query
public interface UserRepository extends JpaRepository<User, Long> {
@Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
nativeQuery = true)
Page<User> findByLastname(String lastname, Pageable pageable);
}
使用排序 Sort
排序可以通过提供 PageRequest 或直接使用 Sort 来完成。
使用命名参数
默认情况下,Spring Data JPA 使用基于位置的参数绑定,如之前的示例。这使得查询方法在重构参数位置时有点容易出错。为了解决这个问题,可以使用@param 注释给方法参数一个具体的名称,并在查询中绑定该名称,如下:
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
User findByLastnameOrFirstname(@Param("lastname") String lastname,
@Param("firstname") String firstname);
}
修改查询
声明操作查询
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
@Modifying 会触发更新查询
Specifications 查询
JPA 2引入了一个标准 API,您可以使用它以编程方式构建查询。通过编写条件,可以为实体类定义查询的 where 子句。
使用 JpaSpecificationExecutor 接口扩展存储库接口
public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor {
…
}
附加的接口具有允许您以各种方式执行规范方法。例如,findAll 方法返回所有与规范匹配的实体,如下面的例子所示:
Specification 接口的定义如下:
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder);
}
**Customer类的 Specifications **
public class CustomerSpecs {
public static Specification<Customer> isLongTermCustomer() {
return new Specification<Customer>() {
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
LocalDate date = new LocalDate().minusYears(2);
return builder.lessThan(root.get(Customer_.createdAt), date);
}
};
}
public static Specification<Customer> hasSalesOfMoreThan(MonetaryAmount value) {
return new Specification<Customer>() {
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
// build query here
}
};
}
}
使用 specification
List<Customer> customers = customerRepository.findAll(isLongTermCustomer());
可以组合多个 specification 对象来创建新的 Specification 对象
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
isLongTermCustomer().or(hasSalesOfMoreThan(amount)));
Example 查询
通过 Example 查询由三部分组成:
- Probe: 有填充字段的实体对象的实际示例。
ExampleMatcher:包含如何匹配特定字段的详细信息,可重用。Example由Probe和ExampleMatcher组成,用于创建查询。
Example 查询非常适合的情况:
- 使用一组静态或动态约束来查询数据存储。
- 频繁地重构域对象,而不必担心破坏现有的查询。
- 独立于底层数据存储 API 工作。
通过示例查询也有几个限制:
- 不支持嵌套或分组的属性约束,如
firstname = ?0 or (firstname = ?1 and lastname = ?2) - 只支持字符串的 start/contains/ends/regex 匹配和其他属性类型的精确匹配。
在开始使用 Query by Example 之前,您需要有一个域对象。首先,为你的 Repository 创建一个接口,如下面的例子所示:
创建 Person对象
public class Person {
@Id
private String id;
private String firstname;
private String lastname;
private Address address;
// … getters and setters omitted
}
前面的示例显示了一个简单的域对象。您可以使用它来创建一个 Example。默认情况下,具有 null 的字段将被忽略,字符串将使用特定于存储的缺省值进行匹配。示例可以通过使用 of 工厂方法或使用 ExampleMatcher 来构建。Example 是不可变的。下面的清单显示了一个简单的例子:
简单的 Example 实例
Person person = new Person(); // 创建域对象的实例
person.setFirstname("Dave"); // 设置要查询的属性
Example<Person> example = Example.of(person); // 创建 Example
Example 查询可以使用存储库执行。为此,让存储库接口扩展 QueryByExampleExecutor<t> 。下面的清单显示了 QueryByExampleExecutor 接口的摘录:
public interface QueryByExampleExecutor<T> {
<S extends T> S findOne(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example);
// … more functionality omitted.
}
Example 匹配器
Example 不仅限于默认设置。您可以使用 ExampleMatcher 进行字符串匹配、空处理和制定具体属性的默认值,如下面的示例所示:
具有定制匹配的 Example matcher
Person person = new Person(); // 创建域对象的实例
person.setFirstname("Dave"); // 设置属性
ExampleMatcher matcher = ExampleMatcher.matching() // ①
.withIgnorePaths("lastname") // ②
.withIncludeNullValues() // ③
.withStringMatcherEnding(); // ④
Example<Person> example = Example.of(person, matcher); // ⑤
① 创建 ExampleMatcher 来匹配期望值,当前即可用
② 构建一个新的 ExampleMatcher,忽略 lastname 属性路径
③ 构建一个新的ExampleMatcher , 忽略lastname 属性路径并包含空值
④ 构建一个新的 ExampleMatcher,忽略 lastname 属性路径,并包含空值,并执行后缀字符串匹配
⑤ 基于域对象和已配置的ExampleMatcher 创建新对象
默认情况下,ExampleMatcher 期望 Probe 上设置的所有值都匹配。
您可以为单个属性指定行为(例如“ firstname”和“ lastname” ,或者对于嵌套属性指定“ address.city”)。你可以通过匹配选项和大小写敏感性来调整它,如下面的例子所示:
我觉得我 Macher 比 specification 复杂,所以后续不再翻译,因为实际开发很少用
3.3 事务性
默认情况下,存储库实例上的 CRUD 方法是事务性的。对于读操作,事务配置 readOnly 标志设置为 true。所有其他的都配置了一个普通的 @transactional,以便应用默认的事务配置。
CRUD 的自定义事务配置
public interface UserRepository extends CrudRepository<User, Long> {
@Override
@Transactional(timeout = 10)
public List<User> findAll();
// Further query method declarations
}
这样做会导致 findAll ()方法在运行时超时10秒,并且不使用 readOnly 标志。
查询方法的事务
为了让你的查询方法具有事务性,在你定义的存储库接口上使用 @transactional,如下面的例子所示:
在查询方法中使用@transactional
@Transactional(readOnly = true)
public interface UserRepository extends JpaRepository<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 的情况下运行。
3.4 Locking 加锁
要指定要使用的锁模式,可以在查询方法上使用 @lock 注释,如下面的示例所示:
定义查询方法上的锁
interface UserRepository extends Repository<User, Long> {
// Plain query method
@Lock(LockModeType.READ)
List<User> findByLastname(String lastname);
}
此方法声明使被触发的查询配备了一个读取的 LockModeType。你也可以通过在你的库接口中重新声明它们并添加 @lock 注释来定义 CRUD 方法的锁,如下面的例子所示:
在 CRUD 方法上定义锁
interface UserRepository extends Repository<User, Long> {
// Redeclaration of a CRUD method
@Lock(LockModeType.READ);
List<User> findAll();
}