SpringDataJpa居然这么简单!!!

92 阅读12分钟

Spring Data JPA 简介和入门

概述

Spring Data JPA 是更大的 Spring Data 系列的一部分,它可以轻松实现基于 JPA 的存储库。该模块处理对基于 JPA 的数据访问层的增强支持。它使构建使用数据访问技术的 Spring 驱动的应用程序变得更加容易。

在相当长的一段时间里,实现应用程序的数据访问层一直很麻烦。必须编写太多样板代码来执行简单的查询以及执行分页和审核。Spring Data JPA 旨在通过将工作量减少到实际需要的数量来显着改进数据访问层的实现。作为开发人员,只需要编写存储库接口,包括自定义查找器方法,Spring Data JPA 将自动提供实现。

特征

  • 支持 Querydsl 谓词,从而支持类型安全的 JPA 查询
  • 域类的透明审计
  • 分页支持、动态查询执行、集成自定义数据访问代码的能力
  • 在引导时验证 @Query 带注释的查询
  • 支持基于 XML 的实体映射
  • 通过引入 @EnableJpaRepositories 基于 JavaConfig 的存储库配置。

使用

引入 maven 依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
   <version>2.7.17</version>
</dependency>

在 application.propertis 文件中添加相关配置

spring.jpa.hibernate.ddl-auto=update #项目启动的时候会通过定义的实体类去检验是否和数据库表匹配,如果不匹配会自动执行一些 DDL 语句去调整数据库表
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=false #禁用懒加载

核心概念

Spring Data 存储库抽象的中心接口是 Repository。这个接口主要充当标记接口,用于捕获要使用的类型,这个接口有很多不同实现,不同实现对应了不同操作数据库的方法,接口 Diagram 图如下

我们的业务 dao 层接口只需要实现这些接口继承接口的方法,可以极大的减少我们的工作量,例子如下:

public interface UserRepository extends CrudRepository<Employee, Integer> {
}
// EmployeesRepository 实现了这个接口就有了用 crud 的功能了
// CrudRepository 接口源码如下
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
// T 对应了数据表实体, ID 是数据表的主键类型
  <S extends T> S save(S entity);
  <S extends T> Iterable<S> saveAll(Iterable<S> entities);
  Optional<T> findById(ID id);
  boolean existsById(ID id);
  Iterable<T> findAll();
  Iterable<T> findAllById(Iterable<ID> ids);
  long count();
  void deleteById(ID id);
  void delete(T entity);
  void deleteAllById(Iterable<? extends ID> ids);
  void deleteAll(Iterable<? extends T> entities);
  void deleteAll();
}

同理还可以可以试下其他的接口如 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));

定义 Repository 接口

标准的 CRUD Repository 通常有对底层数据 store 的查询。使用 Spring Data,声明这些查询成为一个四步过程。

  • 声明一个扩展 Repository 或其子接口之一的接口,并将其泛型指定为它应该处理的 domain 类和 ID 类型,典型的用法就是继承 CrudRepository 接口,它为你提供 crud 的方法如以下例子所示。
interface PersonRepository extends CurdRepository<Person, Long> { … }
//domain -> Person
//ID     -> Long
  • 如果你不想你的接口暴露你不会用到的方法, 你还可以直接在类上添加 @RepositoryDifination 而不继承 Repository 的子接口,具体使用方法如下
@RepositoryDefinition(domainClass = Person.class, idClass = Long.class)
public interface DepartmentsRepository {
  long count();
  void deleteById(Long id);
  void delete(Person entity);
  void deleteAllById(Iterable<? extends Long> ids);
}
  • 你想使用什么方法可以去 Respository 的子接口复制,你还可以尝试更改方法的返回值,如果有可能的话 Spring Data 会尊重你的返回类型,例如,对于返回多个实体的方法,你可以选择 Iterable<T>List<T>Collection<T>
  • 如果你的应用程序中的许多 repository 应该有相同的方法集,你可以定义你自己的基础接口来继承。这样的接口必须用 @NoRepositoryBean 来注释。这可以防止 Spring Data 试图直接创建它的实例而导致异常,因为它仍然包含一个泛型变量,Spring data 无法确定该 repository 的实体。
  • 下面的例子展示了如何有选择地公开 CRUD 方法(本例中为 findByIdsave )。
@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);
}

MyBaseRepository 接口被注解为 @NoRepositoryBean。确保你在所有 Spring Data 不应该在运行时创建实例的 repository 接口上添加该注解。

定义 Query 方法

repository 代理有两种方法可以从方法名中推导出 repository 特定的查询。

  • 下列策略可用于 repository 基础设施解析查询。你可以使用 @EnableJpaRepositories 注解的 queryLookupStrategy 属性。有些策略可能不支持特定的 datastore。
  • 内置在 Spring Data repository 基础架构中的查询 builder 机制对于在资源库的实体上建立约束性查询非常有用。

下面的例子展示了如何创建一些查询。

interface PersonRepository extends Repository<Person, Long> {
  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
  • 解析查询方法名称分为主语和谓语。第一部分( find…By, exists…By)定义了查询的主语,第二部分形成谓语。引入句(主语)可以包含进一步的表达。在 find(或其他引入关键词)和 By 之间的任何文本都被认为是描述性的,除非使用一个限制结果的关键词,如 Distinct 在要创建的查询上设置一个不同的标志,或 Top / First 来限制查询结果
  • 附录中包含了 查询方法主语关键词 和 查询方法谓语关键词的完整列表,包括排序和字母修饰语。然而,第一个 By 作为分界符,表示实际条件谓词的开始。在一个非常基本的层面上,你可以在实体属性上定义条件,并用 AndOr 来连接它们。
  • 解析方法的实际结果取决于你为之创建查询的持久性 store。然而,有一些东西需要注意。
  • 为了处理你的查询中的参数,定义方法参数,正如在前面的例子中已经看到的。除此之外,基础设施还能识别某些特定的类型,如 PageableSort,以动态地将分页和排序应用于你的查询。下面的例子演示了这些功能。

在查询方法中使用 PageableSliceSort

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

API 中定义的 SortPageable 实际调用时不能为 null。如果你不想应用任何排序或分页,请使用 Sort.unsorted()Pageable.unpaged()

  • 第一个方法让你把 org.springframework.data.domain.Pageable 实例传递给 query 方法,以动态地将分页添加到你静态定义的查询中。一个 Page 知道可用的元素和页面的总数。它是通过基础设施触发一个 count 查询来计算总数量。由于这可能是昂贵的(取决于使用的 store),你可以返回一个 Slice。一个 Slice 只知道下一个 Slice 是否可用,当遍历一个较大的结果集时,这可能就足够了。
  • 排序选项也是通过 Pageable 实例处理的。如果你只需要排序,在你的方法中加入 org.springframework.data.domain.Sort 参数。正如你所看到的,返回一个 List 也是可能的。在这种情况下,构建实际的 Page 实例所需的额外元数据并没有被创建(这反过来意味着不需要发出额外的 count 查询)。相反,它限制了查询,只查询给定范围的实体。

分页和排序

  • 捏可以通过使用属性名称来定义简单的排序表达式子。你可以将表达式连接起来,将多个 criteria 收集到一个表达式中。
Sort Sort = Sort.by("firstname").ascending()
   .and(Sort.by("lastname").descending());
//or
TypeSort<Person> person = Sort.sort(Person.class);
Sort soert = person.by(Person::getFirstname).ascending()
   .and(person.by(Person::getLastname).descending());
// or
Qsort sort = QSort.by(QPerson.firstname.asc())
   .and(QSort.by(QPerson.lastname.desc());
    // 如果你的 store 实现支持 Querydsl,你也可以使用生成的 metamodel 类型来定义排序表达式。
  • 你可以通过使用 firsttop 关键字来限制查询方法的结果,这两个关键字可以互换使用。你可以在 topfirst 后面附加一个可选的数值,以指定要返回的最大结果大小。如果不加数字,就会假定结果大小为 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 关键字将结果包入。
  • 如果分页或 slice 应用于 limit 查询的分页(以及可用页数的计算),则会在 limit 结果中应用。

表达式通常是属性遍历与可以串联的运算符的组合。你可以用 ANDOR 来组合属性表达式。你还可以得到对属性表达式的运算符的支持,如 BetweenLessThanGreaterThan、 和 Like。支持的运算符可能因 datastore 的不同而不同,所以请查阅参考文档的适当部分。

方法解析器支持为单个属性(例如, findByLastnameIgnoreCase(…))或支持忽略大小写的类型的所有属性(通常是字符串实例—例如, findByLastnameAndFirstnameAllIgnoreCase (…))设置忽略大小写标志。是否支持忽略大小写可能因 store 而异,所以请查阅参考文档中的相关章节,了解特定 store 的查询方法。

你可以通过在引用属性的查询方法中附加一个 OrderBy 子句,并提供一个排序方向( AscDesc)来应用静态排序。要创建一个支持动态排序的查询方法,请参阅 “特殊参数处理”。

public interface UserRepository extends CrudRepository<Employee, Integer> {
     @Query("SELECT u FROM User u WHERE u.age >= :age")
    List<User> findByAgeGreaterThanEqual(@Param("age") int age);
}
public interface UserRepository extends CrudRepository<Employee, Integer> {
    User findByLastname(String lastname);
    // 会自动生成 select * from User where lastname =? 
    // 具体语句查看控制台日志
}

QueryLookupStrategy.Key.CREATE 表示创建查询方法。当使用方法名查询时,将根据方法名称创建查询方法。这是 Spring Data JPA 的默认行为。

QueryLookupStrategy.Key.USE_DECLARED_QUERY 表示使用已声明的查询。在 Repository 接口中,如果使用 @Query 注解声明了自定义查询,将使用该自定义查询,而不是根据方法名创建查询方法。注意,如果没有找到与方法名匹配的已声明查询,则会引发异常

QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND (默认) 结合了 CREATEUSE_DECLARED_QUERY。它首先查找一个已声明的查询, 如果没有找到已声明的查询, 它将创建一个基于方法名的自定义查询。这是默认的查询策略,因此,如果你没有明确地配置任何东西,就会使用这种策略。它允许通过方法名快速定义查询,但也可以根据需要通过引入已声明的查询对这些查询进行自定义调整。

  • 通过直接从方法名派生出查询。
  • 通过使用手动定义的查询。

其他

  • 限制返回结果
  • 特殊参数处理
  • Query 创建
  • Query 的查询策略

Spring Data JPA 还提供了丰富的返回类型,如 Streambale;还提供了 Repository 方法的 Null 处理功能;它还支持异步查询,具体细节请查看springdoc.cn/spring-data…

创建 Repository 实例

  • Java 配置
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {
  // EntityManagerFactory 是 JPA 中重要的组件之一,
  //它负责加载和管理持久化单元的配置,创建和管理 EntityManager,
  //提供元数据信息,管理缓存和数据库连接等。通过使用 EntityManagerFactory,
  //可以实现对实体对象的持久化操作,并提供性能优化和资源管理的能力。
  @Bean
  EntityManagerFactory entityManagerFactory() {
    // 
  }
}

Jakarta Persistent

简介

Jakarta Persistence(以前称为 Java Persistence API 或 JPA)是一种用于在 Java 应用程序中实现对象关系映射(ORM)的规范和 API。它提供了一种标准化的方式来管理关系型数据库中的对象数据,并将对象模型与数据库模型之间进行映射。Jakarta Persistence 旨在简化应用程序开发人员与数据库之间的交互,提供了一组注解和接口,用于定义实体类、映射关系、查询语言等。通过使用 Jakarta Persistence,开发人员可以通过面向对象的方式来操纵数据库,而不必直接编写 SQL 语句。

使用

常用注解

  • @Entity 用于标识一个类作为实体类(Entity Class)。实体类是映射到数据库表的 Java 类, @Entity 注解将普通的 Java 类标记为可持久化的实体。它的 name 属性可以指定与之对应的数据库表
  • @Id 注解用于标识实体类的主键属性(Primary Key)。它指示该属性将作为实体类在数据库中的唯一标识。
@Entity
public class Employee {
    @Id 
    long empId;
    String empName;
    //...
}
  • @GeneratedValue 注解用于指定实体类的主键生成策略。该注解配合主键字段使用,可以自动生成主键值。

常见的主键生成策略包括:

  • GenerationType.IDENTITY:使用数据库自增长列作为主键。在支持自增长列的数据库中(如 MySQL),可以使用该策略。示例:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
  • GenerationType.SEQUENCE:使用数据库序列生成主键。在支持序列的数据库中(如 Oracle),可以使用该策略。示例:
@Id    
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence-generator")
@SequenceGenerator(name = "sequence-generator", sequenceName = "my_sequence")
private Long id;
  • GenerationType.TABLE:使用数据库表模拟序列生成主键。该策略会创建一个表来保存当前主键值,并通过数据库锁定机制保证唯一性。示例:
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "table-generator")
@TableGenerator(name = "table-generator", table = "id_table",