Spring Data JPA

864 阅读16分钟

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,声明这些查询将使用三个过程:

  1. 声明一个继承 Repository 或其子接口的接口,并添加它应该处理的实体类和 ID 类型,如下面的示例所示:
interface PersonRepository extends Repository<Person, Long> { … }
  1. 在接口上声明查询方法。
interface PersonRepository extends Repository<Person, Long> {
  List<Person> findByLastname(String lastname);
}
  1. 注入存储库实例并使用它,如下面的示例所示:
class SomeClient {
  @autowired
  private PersonRepository repository;

  void doSomething() {
    List<Person> persons = repository.findByLastname("Matthews");
  }
}

下面详细讲解了每个步骤

3.1 定义存储库接口

首先,定义实体类特定的存储库接口。该接口必须继承 Repository 并且类型指定为实体类和实体类的 ID 类型。如果希望公开该实体类的 CRUD 方法,请继承 CrudRepository 而不是 Repository

通常,您的存储库接口扩展 RepositoryCrudRepositoryPagingAndSortingRepository。如果不想扩展 Spring Data 接口,也可以使用 @repositorydefinition 注释存储库接口。

下面的示例演示如何有选择地公开 CRUD 方法(在本例中为 findByIdsave) :

选择显示 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... Byread... Byquery... Bycount... By,和get... By 。这些方法可以包含其他表达式,例如在要创建的查询上设置 Distinct 标志。第一个 By 用作分隔符,表示条件的开始,后面定义实体属性的各种条件,并将它们用 AndOr 连接起来。下面的示例演示如何创建大量查询:

从方法名创建查询

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);
}

需要注意如下:

  • 查询方法表达式通常包括实体类属性与操作符。可以用 ANDOR 将属性表达式组合在一起。还支持诸如 BetweenLessThanGreaterThanLike 之类的操作符。操作符因数据存储类型而异,因此请参考参考文档的相应部分。
  • 方法解析器支持为单个属性设置 IgnoreCase 标志(例如,findByLastnameIgnoreCase (...)),或支持忽略大小写的类型的所有属性(通常是 String 实例),例如 findByLastnameAndFirstnameAllIgnoreCase (...)。是否支持忽略大小写可能因存储类型而异,因此请参考参考文档中关于特定存储的查询方法部分。
  • 您可以将 OrderBy 子句附加到查询方法并提供排序方向(AscDesc)来实现静态排序。若要创建支持动态排序的查询方法,请参阅“特殊参数处理”。

属性表达式

属性表达式只能引用实体的直接属性。在创建查询时,请确保解析的属性是实体类的属性。也可以通过遍历嵌套属性来定义约束。考虑下面的方法签名:

List<Person> findByAddressZipCode(ZipCode zipCode);

假设 PersonAddressZipCode 属性。在这种情况下,该方法将创建属性遍历 x.address.zipCode。解析算法首先将整个部分(AddressZipCode)解释为属性,并检查具有该名称的属性的实体类(非大写)。如果解析成功,它将使用该属性。如果没有,算法会将驼峰式变量从右侧分割为头部和尾部,并尝试找到相应的属性ーー如示例中会被分为 AddressZipCode。算法继续分割头部,找到属性,它就会取下尾部,然后继续从那里构建树,按照刚才描述的方式将尾部向上分割。如果这种拆分不匹配,则算法将拆分点移到左侧(AddressZipCode)并继续执行如上操作。

虽然这在大多数情况下都可以工作,但是算法可能会选择错误的属性。假设 Person 类也有一个 addressZip 属性。算法将在第一轮分割中匹配,选择错误的属性,并失败(因为 addressZip 的类型可能没有代码属性)。

要解决这种不确定性,可以在方法名称中使用 _ 来手动定义遍历点。所以我们的方法名如下:

List<Person> findByAddress_ZipCode(ZipCode zipCode);

因为我们将下划线字符视为保留字符,所以我们强烈建议遵循标准的 Java 命名约定(也就是说,在属性名称中不使用下划线,而是使用驼峰式大小写)。

特殊参数处理

若要处理查询中的参数,请参照前面的示例中那样定义方法参数。除此之外,查询方法还可以识别某些特定类型,如 pagableSort,可以使用他们对查询应用动态地分页和排序。下面的例子展示了这些特性:

在查询方法使用 PageableSort

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 从请求参数中解析 pagableSort 实例。

使用 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 (,ASCDESC)(,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_pagething2_page

传递到该方法的默认 pagable 相当于 PageRequest.of (0,20) ,但可以通过使用 pagable 参数上的@pageabledefault 注释进行自定义。

限制查询结果

查询方法的结果可以通过使用前两个关键字来限制,这两个关键字可以互换使用。可以将一个可选的数值附加到 top 或 first 以指定要返回的最大结果大小。如果省略该数字,则假定结果大小为1。下面的示例演示如何限制查询大小:

TopFirst 限制查询的结果大小

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.Optional
  • scala.Option
  • io.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:包含如何匹配特定字段的详细信息,可重用。
  • ExampleProbeExampleMatcher 组成,用于创建查询。

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();
}