Spring data JPA实践和原理浅析

开篇词

从我们进入编程的世界,成为程序员到现在为止,总有几个感觉神奇和激动的时刻,其中肯定包括你第一次程序连上数据库可以实现CURD功能的时候,就算那时的我们写着千遍一律JDBC模板代码也是乐此不疲。
时代在不断进步,技术也不断在发展,市面上已经有很多优秀的数据库持久化框架供我们使用,今天我将带大家来了解JPA的使用。

1. 基本使用

在我们现在Spring Boot横行无忌的时代,在项目中引入JPA非常简单,我们以Maven以及常用的MySQL数据库为例
在pom.xml文件添加以下依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.6</version>
    <relativePath/>
</parent>

<!-- jpa -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- mysql -->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>

在Spring Boot的YML文件中添加以下内容

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/sakila?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
    username: root
    password: root

定义Entity类,我测试过程使用的数据源为MySQL官方提供的样例数据库sakila,大家可以在https://dev.mysql.com/doc/index-other.html自行下载

@Entity
@Data // lombok注解
public class Actor {

  	@Id
  	// Column注解不是必须的,如果满足字段驼峰形式
  	// 与数据库字段以下划线分隔形式对应即可
    @Column(name = "actor_id", nullable = false)
    private Integer actorId;
  
  	@Column(name = "first_name", nullable = false, length = 45)
    private String firstName;
    private String lastName;
    private Timestamp lastUpdate;
}

定义Respository接口,一般我们通过继承JpaRepository接口即能满足我们一般的CURD操作,如果需要支持复杂逻辑查询,比如动态SQL、联表查询,则需要继承JpaSpecificationExecutor接口,并配合Specification的接口方法。

@Repository
public interface ActorRepository extends JpaRepository<Actor, Integer> {
}

在JpaRepository的中我们可以看到有findAll、getById、findById、save、saveAll、deleteById…这些默认定义,这是JPA为我们提供的常用的一些数据操作,我们可以直接使用,极大提高了日常的开发效率。

1.1 通过方法名称直接生成查询

正是因为这一便利的让人着迷的特性,让我们乐于去使用JPA这一持久化框架,接下来让我们通过几个例子去欣赏它的迷人之处吧。

1.1.1 一般写法
/** 
 * 示例1
 * SQL: SELECT * FROM actor WHERE first_name = ?
 * 参数名的定义不影响程序的运行
 */
List<Actor> findByFirstName(String name);

/**
 * 示例2
 * SQL: SELECT * FROM actor WHERE first_name = ? AND last_name = ?
 * 如果明确知道查询结果返回唯一一条记录时,建议使用单一实体类作为返回类型
 */
List<Actor> findByFirstNameAndLastName(String name1, String name2);

/**
 * 示例3
 * SQL: SELECT * FROM actor WHERE actor_id <= ?
 */
List<Actor> findByActorIdLessThanEqual(Integer id);

遵照JPA的规范,通过定义类似以上接口方法的形式就可以零SQL实现我们需要的单表**查询(不能实现DML操作)**操作。JPA对此类查询方式有很丰富的支持,受限于篇幅,我们就不一一讲述了,详细的内容可以阅读官方文档,地址:https://docs.spring.io/spring-data/jpa/docs/2.5.6/reference/html/#repository-query-keywords

Tips

  1. 在查询场景中,自定义的查询接口中,find关键词(也可以是search、query、get)后面必须跟随By关键词
  2. Between适用于数值、日期字段,用于日期时,参数类型可以是java.util.Date或java.sql.Timestamp
List<Film> findByLengthBetween(Integer low, Integer up);
List<Film> findByLastUpdateBetween(Date startDate, Date endDate);
  1. IsEmpty / IsNotEmpty只能用于集合类型的字段
  2. Before或者After可用于日期、数值类型的字段
List<Film> findByLengthBefore(Integer length)
  1. 涉及到删除和修改时,需要在方法定义上加上@Modifying
1.1.2 分页

在我们日常的使用,分页是很常见的场景,在JPA中我们可以这样做:

/**
 * 1.在分页场景下,只需要在方法定义中传入Pageable类型的参数即可,参数的位置没有限制.
 * 2.实际上返回类型可以指定为List<Actor>,但是会丢失总记录数、分页数等信息,仅保留数据.
 */
Page<Actor> findByActorIdLessThanEqual(Pageable pageable, Integer id);
1.1.3 排序

当然,我们可能也少不了需要排序,常用的有以下几种形式

/**
 * 示例1
 * 使用方法名申明的形式
 * SQL: SELECT * FROM actor WHERE first_name = ? ORDER BY actor_id DESC
 * 如果你有多个字段需要排序,你可以这样表示:findByFirstNameOrderByActorIdActorIdDescFirstNameAsc
 */
List<Actor> findByFirstNameOrderByActorIdDesc(String firstName)
    
/**
 * 示例2
 * 定义Sort类型的入参
 */
List<Actor> findFirst10By(Sort sort);

/**
 * 示例3
 * 如果在分页的情况下,sort相关的配置可以在构建Pageable实例时一并设置
 */
final Sort.Order byId = Sort.Order.desc("actor_id");
final Sort sort = Sort.by(byId);
final PageRequest pageRequest = PageRequest.of(0, 10, sort);

1.2 基于@Query 注解的操作

1.2.1 使用JPQL

JPQL是通过Hibernate的HQL演变过来的,它和HQL语法及其相似。

/** 
 * 示例1
 * SQL: SELECT * FROM actor WHERE first_name = ?
 */
@Query("FROM Actor WHERE firstName = ?1")
List<Actor> findByFirstName(String name);

/**
 * 示例2
 * SQL: SELECT * FROM actor WHERE first_name = ? AND last_name = ?
 */
@Query("FROM Actor WHERE firstName = ?1 AND lastName = ?2")
List<Actor> findByFirstNameAndLastName(String name1, String name2);

/**
 * 示例3
 * SQL: SELECT * FROM actor WHERE actor_id <= ?
 */
@Query("FROM Actor WHERE actorId <= ?1")
List<Actor> findByActorIdLessThanEqual(Integer id);

/**
 * 示例4
 * SQL: SELECT * FROM actor
 * 不能写"SELECT *" 要写"SELECT 别名"
 */
@Query("SELECT a FROM Actor a")
List<Actor> findAll()

通过以上例子我们发现,JPQL与SQL的区别是,SQL是面向对象关系数据库,它操作的是数据表和数据列,而JPQL操作的对象是实体对象和实体属性,区分大小写,出现的SQL关键字还是原有的意思,不区分大小写。JPQL也可以支持复杂的联表查询。下面是JPQL的基本格式:

SELECT 实体别名.属性名, 实体别名.属性名 FROM 实体名 AS 实体别名 WHERE 实体别名.实体属性 op 比较值

看完查询的写法,我们来看看DML操作的写法

@Modifying
@Query(value = "UPDATE Film SET description = :description WHERE filmId = :id")
void update(@Param("id") Integer filmId, @Param("description") String description);

1.2.2 使用原生SQL

在Query注解中,有一个属性字段nativeQuery,默认情况下为false,即为JPQL模式,如果我们设置为true,则我们可以value属性中定义原生SQL语句实现数据库操作

@Query("SELECT * FROM actor WHERE first_name = ?1", nativeQuery = true)
List<Actor> findByFirstName(String name);

@Query("SELECT * FROM actor WHERE first_name = ?1 AND last_name = ?2", nativeQuery = true)
List<Actor> findByFirstNameAndLastName(String name1, String name2);

@Query("SELECT * FROM actor WHERE actor_id <= ?", nativeQuery = true)
List<Actor> findByActorIdLessThanEqual(Integer id);

@Query("SELECT first_name, last_name FROM actor", nativeQuery = true)
List<Map<String, Object>> findAll();
// List<String[]> findAll();

通过上述示例,我们总结几点编写原生SQL需要注意的地方

  1. 如果接口方法返回的是实体对象,如List、Actor这样的,则SELECT部分不能指定字段名,必须为*
  2. 如果只想查询指定列的数据,方法定义时的返回类型可以是Map<String, Object>、String[],返回数据如果是多条,则用集合类嵌套
  3. 关于JPA接口方法的返回类型我们可以参考官方文档,地址为:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-return-types

1.3 使用Specification实现复杂操作

在我们日常的开发过程中,会遇到一些复杂的查询逻辑,比如动态拼接查询条件,这时我们需要借助JPA提供的JpaSpecificationExecutor接口配合Specification来实现我们的需求。
首先我们需要让Repository具备使用Specification的能力,即需要继承JpaSpecificationExecutor接口

@Repository
public interface FilmRepository extends JpaRepository<Film, Integer>, 
	JpaSpecificationExecutor<Film> {
}

通过查看JpaSpecificationExecutor接口的定义,我们发现接口方法中多了一个Specification类型的参数,这将是我们实现动态SQL的关键接口

public interface JpaSpecificationExecutor<T> {

	/**
	 * Returns a single entity matching the given {@link Specification} or {@link Optional#empty()} if none found.
	 *
	 * @param spec can be {@literal null}.
	 * @return never {@literal null}.
	 * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found.
	 */
	Optional<T> findOne(@Nullable Specification<T> spec);

	/**
	 * Returns all entities matching the given {@link Specification}.
	 *
	 * @param spec can be {@literal null}.
	 * @return never {@literal null}.
	 */
	List<T> findAll(@Nullable Specification<T> spec);

	/**
	 * Returns a {@link Page} of entities matching the given {@link Specification}.
	 *
	 * @param spec can be {@literal null}.
	 * @param pageable must not be {@literal null}.
	 * @return never {@literal null}.
	 */
	Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);

	/**
	 * Returns all entities matching the given {@link Specification} and {@link Sort}.
	 *
	 * @param spec can be {@literal null}.
	 * @param sort must not be {@literal null}.
	 * @return never {@literal null}.
	 */
	List<T> findAll(@Nullable Specification<T> spec, Sort sort);

	/**
	 * Returns the number of instances that the given {@link Specification} will return.
	 *
	 * @param spec the {@link Specification} to count instances for. Can be {@literal null}.
	 * @return the number of instances.
	 */
	long count(@Nullable Specification<T> spec);
}

我们直接通过例子来了解Specification使用方式

Specification<Film> specification = (root, criteriaQuery, criteriaBuilder) -> {
    List<Predicate> conditions = new ArrayList<>();
    conditions.add(criteriaBuilder.equal(root.get("releaseYear"), 2006));
    conditions.add(criteriaBuilder.or(
        criteriaBuilder.greaterThan(root.get("rentalDuration"), 5),
        criteriaBuilder.like(root.get("description"), "%A%")));
    conditions.add(criteriaBuilder.isNotNull(root.get("specialFeatures")));
    criteriaQuery.where(conditions.toArray(new Predicate[0]));
    return null;
};

通过示例,我们看到的是熟悉的Java代码,因此我们可以实现自己想要的逻辑,比如入参的有值或无值,枚举型参数的值选择性的在conditions中添加条件。需要注意的是使用root.get()时,参数名需要与Entity中的属性名对应。

2. JPA中的实体(Entity)

通过第一节的介绍,我们了解了JPA的基本使用方法,已经能够满足日常大部分场景的开发需求了。我们也发现一个问题,不管何种JPA接口的使用方式,都与我们的实体类都存在着关系。JPA的众多特性都是围绕实体展开的。
JPA中的实体是一个由@Entiy注解修饰的Java Bean,它描述了业务数据实体与数据库表的映射关系。每个 JPA实体都必须有一个主键字段(使用@Id注解标识),用于从其他实例中唯一地标识它。主键(或复杂主键中包含的字段)必须是持久字段。下面的内容我们将主要介绍实体的一些基本概念和常用的注解。

2.1 Entity中常用的注解

  1. @Entity(name=“xxx”)

表明本类是一个JPA实体,name的默认值就是类名。

  1. @Table(name=“xxx”)

指定和数据库中映射表的名称,如果表名的驼峰式写法与类名相同,这可以省略name的值

  1. @Id

用于表示该属性作为ID主键

  1. @GeneratedValue(strategy=GenerationType.AUTO)

用于指定主键生成策略,与@Id注解配合使用。GenerationType总共有四个:

  • AUTO:表示主键自增长由实现jpa的框架来控制
  • TABLE:由一个表来维护主键,这个表记录上一次生成的主键,然后+1给作为新的主键,这种方式效率比较低
  • SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列
  • IDENTITY:主键增长由数据库来维护,可能不同数据库有不同的策略(主要是自动增长型)
  1. @Column

标识实体类中属性与数据表中字段的对应关系。共有10个属性,这10个属性均为可选属性:

  • name属性定义了被标注字段在数据库表中所对应字段的名称;
  • unique属性表示该字段是否为唯一标识,默认为false。如果表中有一个字段需要唯一标识,则既可以使用该标记,也可以使用@Table标记中的@UniqueConstraint。
  • nullable属性表示该字段是否可以为null值,默认为true。如果属性里使用了验证类里的@NotNull注释,这个属性可以不写。
  • insertable属性表示在使用“INSERT”脚本插入数据时,是否需要插入该字段的值。
  • updatable属性表示在使用“UPDATE”脚本插入数据时,是否需要更新该字段的值。insertable和updatable属性一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的。
  • columnDefinition属性表示创建表时,该字段创建的SQL语句,一般用于通过Entity生成表定义时使用。若不指定该属性,通常使用默认的类型建表,若此时需要自定义建表的类型时,可在该属性中设置。(也就是说,如果DB中表已经建好,该属性没有必要使用。)
  • table属性定义了包含当前字段的表名。
  • length属性表示字段的长度,当字段的类型为varchar时,该属性才有效,默认为255个字符。
  • precision属性和scale属性表示精度,当字段类型为double时,precision表示数值的总长度,scale表示小数点所占的位数。
  1. @Enumerated(EmumType.STRING)

用于标识该枚举属性对应的数据库字段存储的是ordinal值还是name值。在后面的小节我会对枚举字段做更详细的介绍。EmumType总共有两个值:

  • STRING:表示存储的是枚举的name值
  • ORDINAL:表示存储的是枚举的ordinal值
  1. @Transient

表示被修饰的字段在表中没有对应的列,该字段不需要添加到数据库表。请注意是javax.persistence.Transient。但是如果你用的是MongoDB,则需要使用org.springframework.data.annotation.Transient

介绍了这么多,我们来看一下实际使用的例子

@Entity
@Table(name = "customer")
@Data
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "customer_id", nullable = false)
    private Integer customerId;

    @Column(name = "first_name", nullable = false, length = 45)
    private String firstName;

    @Column(name = "last_name", nullable = false, length = 45)
    private String lastName;

    @Column(name = "email", nullable = true, length = 50)
    private String email;

    @Enumerated(EnumType.ORDINAL)
    @Column(name = "active", nullable = false)
    private CustomerStatus active;

    @Column(name = "create_date", nullable = false)
    private Timestamp createDate;

    @Column(name = "last_update", nullable = true)
    private Timestamp lastUpdate;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "store_id", nullable = false)
    private Store store;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "address_id", nullable = false)
    private Address address;

    @Transient
    private String ignoreField;

}

我们见到的注解中,还有一些针对实体间关联关系的注解,比如@ManyToOne、@OneToOne等,在这里我就不做多介绍,我本人是不太建议在团队开发项目中使用这些注解,容易使用不当导致性能问题。

2.2 Entity中的枚举字段

将Enum类型的字段映射到数据库中有两种方式:

  • 一个是通过使用Enum类型实例在Enum中声明的索引顺序,也就是ordinal属性,通过这个序号来将Enum类型字段映射成int类型来存储
  • 一个是通过使用Enum类型实例中的name属性来完成映射,这里将Enum类型映射成String类型来完成存储
// 枚举类
@Getter
public enum CustomerStatus {

    INACTIVE(0), ACTIVE(1);

    private int code;

    CustomerStatus(int code) {
        this.code = code;
    }

    static final Map<Integer, CustomerStatus> map;

    static {
        map = new HashMap<>();
        for (CustomerStatus status : CustomerStatus.values()) {
            map.put(status.code, status);
        }
    }

    public static CustomerStatus resolve(Integer code) {
        return map.getOrDefault(code, INACTIVE);
    }
}


// Entity类
@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer customerId;

    @Enumerated(EnumType.ORDINAL)
    private CustomerStatus active;

    ...

}
2.2.1 不使用注解

如果在枚举属性上不使用注解,则默认使用枚举的ordinal属性值与数据库字段的值映射,比如customer表中某条记录的active字段值为0,则映射到java实体中active字段的值为INACTIVE,反之同理。

2.2.2 使用@Enumerated注解

通过之前的介绍,我们知道注解属性中的EnumType有ORDINAL、STRING两种值,默认情况下为ORDINAL,映射逻辑参考2.2.1的介绍。
如果我们将Customer实体中active属性的注解描述改为@Enumerated(EnumType.STRING),则表示customer表中active字段将存储为INACTIVE、ACTIVE,当然,我们的数据库字段则需要使用VARCHAR类型。

2.2.3 使用AttributeConverter属性类型转换器

关于AttributeConverter<X, Y>

  • X 是实体属性的类型
  • Y 是数据库字段的类型
  • Y convertToDatabaseColumn(X) 作用:将实体属性X转化为Y存储到数据库中,即插入和更新操作时执行
  • X convertToEntityAttribute(Y) 作用:将数据库中的字段Y转化为实体属性X,即查询操作时执行

如果我们想通过CustomerStatus中自己定义的code字段的值与customer表的active映射,Enumerated注解将无法满足我们的需求,我们需要定义一个类来实现AttributeConverter接口:

public class CustomerStatusConverter implements AttributeConverter<CustomerStatus, Integer> {
 
    @Override
    public Integer convertToDatabaseColumn(CustomerStatus attribute) {
        return attribute.getCode();
    }
 
    @Override
    public CustomerStatus convertToEntityAttribute(Integer dbData) {
        return CustomerStatus.resolve(dbData);
    }
}

// Entity类
@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer customerId;

    @Convert(converter = CustomerStatusConverter.class)
    private CustomerStatus active;

    ...

}

另外提一点,AttributeConverter不仅仅是用于枚举的转换,也可以用于定义我们需要的任何转换关系,如某个存储json字符串的字段与项目中自定义的某个实体相互转换。

2.3 Entity的生命周期

在使用JPA的过程中,我们经常会提到Entity,Entity就是在内存中短暂存活,在数据库中被持久化了的对象。Entity和数据库中的表映射,也就是我们常说的ORM。我们可以持久化一个Entity,删除一个Entity或者通过Java Persistence Query Language(JPQL)来查询Entity。 我们在前面看到的被@Entity修饰的Java类就是一个Entity定义。
在JPA中,Entity的整个生命周期中有4种状态:New、Managed、Datached、Removed。我们可以通过一张图来了解他们的转换关系
在这里插入图片描述
瞬时(New):瞬时对象,刚new出来的对象,无id,还未和持久化上下文(Persistence Context)建立关联。
托管(Managed):托管对象,有id,已和持久化上下文(Persistence Context)建立关联,对象属性的所有改动均会影响到数据库中对应记录。

  • 瞬时对象调用em.persist方法之后,对象由瞬时状态转换为托管状态
  • 通过find、get、query等方法,查询出来的对象为托管状态
  • 游离状态的对象调用em.merge方法,对象由游离状态转换为托管状态

游离(Datached):游离对象,有id值,但没有和持久化上下文(Persistence Context)建立关联。

  • 托管状态对象提交事务之后,对象状态由托管状态转换为游离状态
  • 托管状态对象调用em.clear方法之后,对象状态由托管状态转换为游离状态
  • New出来的对象,id赋值之后,也为游离状态

删除(Removed):执行em.remove方法,但未提交事务的对象,有id值,没有和持久化上下文(Persistence Context)建立关联,准备从数据库中删除。

2.4 实体管理器(EntityManager)

在介绍实体的生命周期的内容中,我们看到实体各种状态的转换过程中涉及到了em对象中的方法调用,其实这里的em对象就是EntityManager。
EntityManager 是用来对实体Bean进行操作的辅助类。他可以用来产生/删除持久化的实体Bean,通过主键查找实体Bean,也可以通过QL语言查找满足条件的实体Bean。只有当实体Bean被EntityManager管理时,EntityManager才会跟踪它的状态改变,在任何决定更新实体Bean的时候便会把发生改变的值同步到数据库中。当实体Bean从EntityManager分离后,它是不受管理的,EntityManager无法跟踪它的任何状态改变,但Java对象还会在内存中存在,直到被GC。如果我们想要在我们的Spring Bean中使用EntityManager的对象,可以通过@PersistenceContext注解由EJB容器动态注入。

2.4.1 持久化上下文(Persistence Context)

在介绍EntityManager API之前,我们先来看看Persistence Context的概念。一个Persistence Context就是针对一个事务中一段时间内一群被管理的Entity的集合。多个具有相同唯一标识的Entity实例不能存在于同一个Persistence Context中。例如,一个Customer实例的customerId是1,此时就不能有第二个customerId也是1的Customer实例存在于相同的Persistence Context中了。只有存在于Persistence Context中的Enitity才会被EntityManager所管理,它们的状态才会反映到数据库中。Persistence Context可以被看成一个一级缓存,它可以被EntityManager当作存放Entity的缓存空间。默认情况下,Entity在Persistence Context存活,直到用户的事务结束。
每个事务都有自己的Persistence Context,多个Persistence Context访问同一个数据库的实例如下图:
在这里插入图片描述

2.4.2 EnityManager的接口介绍

我们先来看一个例子

Customer customer = new Customer("Antony", "Balla", "tballa@mail.com");
Address address = new Address("Ritherdon Rd", "London", "8QE", "UK");
customer.setAddress(address);
tx.begin();
em.persist(customer);
em.persist(address);
tx.commit();

上例中的Customer和Address是两个普通的Java对象,当被EntityManager调用了persist方法后,两个对象都变成了EntityManager所管理的Entity。当Transaction提交后,他们的数据会被插入到数据库中。这里的Customer对象是对象关系的持有者,它对应的表结构应当有一个外键来对应Address对象。我们注意一下存储两个对象的顺序。即便是将两个对象存储的顺序颠倒一下,也不会造成外键找不到的错误。之前我们已经说过了,Persistence Context可以被看作一级缓存。在事务被提交之前,所有的数据都是在内存中的,没有对数据库的访问,EntityManager缓存了数据,当数据准备好后,以底层数据库希望的顺序将数据更新到数据库中。
想查找一个Entity,有两个类似的方法,代码如下:

Customer customer = em.find(Customer.class, 1234L);
if (customer!= null) {
// 处理对象
}
  
try {  
    Customer customer = em.getReference(Customer.class, 1234L);
// 处理对象
} catch(EntityNotFoundException ex) {
// Entity没有找到
}

find方法会根据主键返回一个Entity,如果主键不存在数据库中,会返回null。getReference和find方法很类似,但是只是返回一个Entity的引用,不会返回其中的数据。它用于那些我们需要一个Entity对象和它的主键但不需要具体数据的情况。如例所示,当Entity找不到时,会有EntityNotFoundException抛出。
一个Entity可以通过EntityManager.remove()被删除,一但Entity被删除,它在数据库中也会被删除,并且脱离了EntityManager管理(detached状态)。此时这个对象不能再和数据库中的数据同步了。

tx.begin();
em.remove(customer);
tx.commit();

在之前的所有例子中,和数据库的数据的同步都是发生在事务提交时。所待执行的改变都是需要一个SQL语句的执行。大多数情况下,这种和数据库的同步机制能满足我们程序的需要。如果我们想将对Persistence Context中数据改变立刻反映到数据库中,可以通过调用flush方法实现。或者我们想将数据库中的数据重新同步回Persistence Context,可以调用refresh方法。当应用程序在调用了flush方法后,又调用了rollback方法,所有同步到数据库的数据又会都被回滚。
这种同步机制很像我们在命令窗口中直接执行多个SQL语句,当显性调用flush方法时,相当于执行我们已经输入的SQL语句,但没有提交事务。当tx.commit方法调用时,事务才真正的被提交。如果没有调用flush方法,则在tx.commit方法调用时先执行已经输入的SQL语句再提交事务。

tx.begin();
em.persist(address);
em.flush();
em.persist(customer);
tx.commit();

上面这个代码例子中,persist执行的顺序是要被保证的。因为在调用flush方法时,变化已经被同步到数据库中了,即SQL语句已经被执行了,如果两个persist方法顺序颠倒一下,则会出现外键约束的异常。
refresh方法实现的效果可以通过下面的例子显示出来:

Customer customer = em.find(Customer.class, 1234L);
assertEquals(customer.getFirstName(), "Antony");
customer.setFirstName("William");
em.refresh(customer);
assertEquals(customer.getFirstName(), "Antony");

contains方法会返回一个Boolean值,用于检测当前Persistence Context中是否存在某个Entity

Customer customer = new Customer("Antony", "Balla", "tballa@mail.com");
tx.begin();
em.persist(customer);
tx.commit();
assertTrue(em.contains(customer));
tx.begin();
em.remove(customer);
tx.commit();
assertFalse(em.contains(customer));

clear方法可以清空当前Persistence Context,是所有的Entity都变成detached状态。detach方法则是只将某个Entity变成detached状态。前面已经说了detached的Entity不会和数据库中的数据再进行同步了。

Customer customer = new Customer("Antony", "Balla", "tballa@mail.com");
tx.begin();
em.persist(customer);
tx.commit();
assertTrue(em.contains(customer));
em.detach(customer);
assertFalse(em.contains(customer));

如果我们想使一个detached的Entity重新和数据库中的数据进行同步,可以调用merge方法。想象有这样一个场景,我们需要从数据库中取出某个对象,这个对象从持久层传到表现层之前变成了detached状态。在表现层中,Entity的一些数据发生了变化,我们将这个Entity传回持久层并让它变成managed状态以将变化反映到数据库中。

Customer customer = new Customer("Antony", "Balla", "tballa@mail.com");
tx.begin();
em.persist(customer);
tx.commit();
em.clear();
// 设置一个新的值给一个detached的entity
customer.setFirstName("William");
tx.begin();
em.merge(customer);
tx.commit();

通过上面的举例,我们已经熟悉了EntityManager常见接口的用法,接下来我们将通过一个复杂一些例子来学习一下EntityManager的其他方面的使用方法,同时补充一下在实体中会用到的一些注解知识。先描述一下示例的使用场景:

在操作customer表的过程中,我们想要一个根据active的值对数据记录做筛选,并在特定的service逻辑中开启的过滤器

在这个示例中我们将在实体类中使用到FilterDef注解和Filters注解。

1.@FilterDef

用于描述过滤器的定义,主要有2个需要关注的属性:

  • name:对过滤器命名
  • parameters:本身也是注解类型,为数组,用于对过滤器参数的描述,属性name描述参数的名称,属性type描述参数的类型
  • defaultCondition:默认筛选条件(添加到SQL中WHERE部分)

2.@Filters

用于添加需要使用的过滤器,其中唯一的属性是Filter注解类型的数组,表示可以添加多个过滤器。在Filter注解中,我们主要关注name属性和condition属性:

  • name:过滤器名称,需要是FilterDef注解中定义过的名称
  • condition:过滤条件表达式,其中可以使用FilterDef注解中定义的parameter作为占位符。如果为空则表示使用defaultCondition

介绍完基本概念,我们来看具体的示例代码:

// 过滤器定义
@Entity
@FilterDef(name = "filterByActive", parameters = {
        @ParamDef(name = "code", type = "int")
})
@Filters({
        @Filter(name = "filterByActive", condition = "active = :code")
})
@Data
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer customerId;
    
    @Enumerated(EnumType.ORDINAL)
    private CustomerStatus active;
    
    ...
}

// 过滤器开关注解
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface EnableCustomerStatusFilter {
}

// 用于实现动态过滤和开启过滤器的切片
@Aspect
@Component
public class CustomerStatusFilterAdvice {

    @PersistenceContext
    private EntityManager em;

    @Around("@annotation(ai.advance.jpademo.annotations.EnableCustomerStatusFilter)")
    public Object doProcess(ProceedingJoinPoint joinPoint) throws Throwable {
        try{
            // 开启指定过滤器
            Filter filter = em.unwrap(Session.class).enableFilter("filterByActive");
            // 本示例中设置为固定值,在实际使用中可以根据需求实现更复杂的逻辑
            filter.setParameter("code", 1);
            return joinPoint.proceed();
        }catch (Throwable ex){
            ex.printStackTrace();
            throw ex;
        }finally {
            // 关闭指定过滤器
            em.unwrap(Session.class).disableFilter("filterByActive");
        }
    }
}

// 过滤器在service层的使用
@Service
@AllArgsConstructor  //lombok注解
public class CustomerService {

    private final CustomerRepository repository;

    // @EnableCustomerStatusFilter为自定义注解以实现切面开启Filter
    // @Filter是Entity上的注解,在添加该注解之后,hibernate会在相应查询语句中添加WHERE子句以达到过滤的目的
    // @Transactional是使用Filter功能的前提、必要条件
    // 这个过滤对于懒加载不起作用
    // 如果通过主键直接查询,那么过滤器将不起作用
    // 最终SQL为:SELECT * FROM customer WHERE active = ? AND active = 1
    @Transactional
    @EnableCustomerStatusFilter
    public List<Customer> fetchByActive(CustomerStatus status) {
        return repository.findByActive(status);
    }
}

3. JPA的原理浅析

通过前面的学习,我们已经对JPA有了比较清晰的了解,不再是门外汉了,当然我们还是会有很多的疑问。

  • 为什么我们只是加入了一个Maven的依赖,就能直接使用JPA?
  • 为什么我们只是定义了一个Repository的接口就能直接使用它来实现数据库操作逻辑?
  • 为什么我们只是按照JPA的规范,定义了一个接口方法就能在使用时生成想要的SQL?
    这些问题让我们在接下来的学习中一一解开。

3.1 JPA的自动配置

虽然这一节是要讲JPA的自动配置,其实本质要说的是Spring Boot自动加载配置的原理。在我们使用Spring Boot创建一个项目时,必不可少的一个注解就是@SpringBootApplication,看着它我们既熟悉又陌生。我们通过它的源码来一探究竟。

package org.springframework.boot.autoconfigure;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};

	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;

}

通过源码我们发现其实SpringBootApplication也被一些注解所修饰,其中就有EnableAutoConfiguration注解,一看名字我们就知道它就是我们今天要找的正主。同样的,我们也不会放过它的源码的。

package org.springframework.boot.autoconfigure;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	Class<?>[] exclude() default {};

	String[] excludeName() default {};

}

我们又看到了熟悉的身影,@Import注解,它意味着在我们的IOC容器中引入一个AutoConfigurationImportSelector对象,通过源码我们又发现AutoConfigurationImportSelector最终实现了ImportSelector接口。因此在程序的启动过程中,会执行selectImports方法,在当前的实现中,这个方法的主要逻辑是去读取一个 spring.factories下key为EnableAutoConfiguration对应的全限定名的值。
spring.factories里面配置的那些类,主要作用是告诉 Spring Boot这个stareter所需要加载哪些xxxAutoConfiguration类,也就是你真正的要自动注册的那些bean或功能。而在SpringBoot中的META-INF/spring.factories(完整路径:spring-boot/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories)中关于EnableAutoConfiguration的这段配置如下 :

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=

org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,

org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,

可以发现有JpaRepositoriesAutoConfiguration和HibernateJpaAutoConfiguration帮我们配置了JPA 相关的配置。至此,我们的第一个疑惑可以解开了。

3.2 SimpleJpaRepository类

接下来我们要来解决第二个疑惑,其实本节的标题已经告诉了我们答案。先上源码:

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
}

我们不用看具体实现的方法逻辑了,单从SimpleJpaRepository的声明来看,它是一个类,不是抽象类,是一个实现了JpaRepositoryImplementation接口的类,实际上JpaRepositoryImplementation接口也继承了JpaRepository和JpaSpecificationExecutor接口。
到此,我们可以大胆的猜测,为什么我们定义的repository接口只是继承了JpaRepository和JpaSpecificationExecutor接口就能使用一系列的接口调用,其实是SimpleJpaRepository类帮我们做了要做的事情。
那到底是不是,如果是的,那JPA是怎么做到了的呢?我们继续一步一步分析。首先我们得有一个分析的起点,上一节我们讲了JPA的自动配置,那我们是不是需要看看它到底配置了什么,看看能不能找到我们想要的。我们直接看JpaRepositoriesAutoConfiguration,因为它的名字里带Repositories。

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(DataSource.class)
@ConditionalOnClass(JpaRepository.class)
@ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class })
@ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "enabled", havingValue = "true",
		matchIfMissing = true)
@Import(JpaRepositoriesRegistrar.class)
@AutoConfigureAfter({ HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
public class JpaRepositoriesAutoConfiguration {
}

我们好像运气不错,果然发现有一个Import注解引入了一个JpaRepositoriesRegistrar类,从名字看,JPA repository注册器,好像越来越接近了,直接上源码:

class JpaRepositoriesRegistrar extends AbstractRepositoryConfigurationSourceSupport {
}

public abstract class AbstractRepositoryConfigurationSourceSupport
		implements ImportBeanDefinitionRegistrar, BeanFactoryAware, ResourceLoaderAware, EnvironmentAware {

	private ResourceLoader resourceLoader;

	private BeanFactory beanFactory;

	private Environment environment;

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
			BeanNameGenerator importBeanNameGenerator) {
		RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(
				getConfigurationSource(registry, importBeanNameGenerator), this.resourceLoader, this.environment);
		delegate.registerRepositoriesIn(registry, getRepositoryConfigurationExtension());
	}

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		registerBeanDefinitions(importingClassMetadata, registry, null);
	}
    
    // 其他源码
    ...
}

其实分析了这么久,如果我们经常去看看Spring系的一些源码,会发现很多老朋友。我们看到其中一个关键接口ImportBeanDefinitionRegistrar,它的关键方法就是registerBeanDefinitions了,就是按照我们的实现逻辑把一些我们需要的bean注册到IOC容器中。分析到这,我们通过测试代码调试来看看吧。
在这里插入图片描述
我们可以看到debug窗口中,在测试类中注入的CustomerRepository接口的实际对象是JdkDynamicAopProxy类型,这是一个jdk动态代理类,这确实比较符合我们对Spring IOC容器对接口类注入处理的认识。我们看一下它代理的目标类,发现是SimpleJpaRepository类,如此就证实了我们之前的猜测。第一次调试到此结束。
我们再回过头来看我们之前找到的AbstractRepositoryConfigurationSourceSupport类中registerBeanDefinitions方法,这个是在项目启动过程中执行的,我们加上断点进行第二次调试。
registerBeanDefinitions方法中主要的逻辑在delegate.registerRepositoriesIn中实现,我们直接在里面标记上断点。
在这里插入图片描述
当执行到上面断点后,我们看到configurations对象中已经有了我们自己定义的repository接口的信息了,接下来我们将进入for循环分别处理我们定义的接口了,我们进到下一个断点继续看。
在这里插入图片描述
在当前断点,我们可以看到,beanName是符合我们正常创建IOC容器中bean的命名规则的,但是JPA为我们创建的BeanDefinition对象是org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean类型,它是一个FactoryBean。我们继续看187、189行的代码,JPA将我们的BeanDefinition对象设置了一个name为factoryBeanObjectType,value为当前被处理的repository接口的全限定类名(比如ai.advance.jpademo.repository.FilmRepository)的attribute,最后将BeanDefinition对象注册到了我们的IOC容器中。因此当for循环执行完后,如果我们使用repository接口对应的beanName从IOC容器中获取一个实例,最开始我们拿到的是一个JpaRepositoryFactoryBean类型的Bean,当然我们最后真正拿到的bean对象是由getObject方法返回的对象。我们先来看一下它的继承关系:
在这里插入图片描述
JpaRepositoryFactoryBean既是一个工厂,又是一个bean。其作用类似于@Bean注解,但比其能实现更多复杂的功能,可以对对象增强。重要方法是getObject(),返回一个bean,因此我们去看一下getObject方法的实现,实际它的getObject方法的实现在其父类RepositoryFactoryBeanSupport中。

@Nonnull
public T getObject() {
	return this.repository.get();
}

好像没什么东西,有点懵,那我们先忽略它,我们再一想,JpaRepositoryFactoryBean对象也是一个Bean,那有没有一点和bean实例化过程相关的代码,然后我们找到了这块代码:

@Override
public void afterPropertiesSet() {

    Assert.state(entityManager != null, "EntityManager must not be null!");

    super.afterPropertiesSet();
}

而确实JpaRepositoryFactoryBean的父类RepositoryFactoryBeanSupport也实现了InitializingBean接口,它自身也是调用了父类的afterPropertiesSet方法。

// 以下代码实现在org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport中
public void afterPropertiesSet() {

    this.factory = createRepositoryFactory();
    this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey);
    this.factory.setNamedQueries(namedQueries);
    this.factory.setEvaluationContextProvider(
            evaluationContextProvider.orElseGet(() -> QueryMethodEvaluationContextProvider.DEFAULT));
    this.factory.setBeanClassLoader(classLoader);
    this.factory.setBeanFactory(beanFactory);

    if (publisher != null) {
        this.factory.addRepositoryProxyPostProcessor(new EventPublishingRepositoryProxyPostProcessor(publisher));
    }

    repositoryBaseClass.ifPresent(this.factory::setRepositoryBaseClass);

    this.repositoryFactoryCustomizers.forEach(customizer -> customizer.customize(this.factory));

    RepositoryFragments customImplementationFragment = customImplementation //
            .map(RepositoryFragments::just) //
            .orElseGet(RepositoryFragments::empty);

    RepositoryFragments repositoryFragmentsToUse = this.repositoryFragments //
            .orElseGet(RepositoryFragments::empty) //
            .append(customImplementationFragment);

    this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface);

    this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));

    // Make sure the aggregate root type is present in the MappingContext (e.g. for auditing)
    this.mappingContext.ifPresent(it -> it.getPersistentEntity(repositoryMetadata.getDomainType()));

    if (!lazyInit) {
        this.repository.get();
    }
}

逻辑有点多,我们先看到这一行代码:

this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));

先提一句Lazy类继承了java.util.function.Supplier接口。这个this.repository我们是不是在getObject方法有看到,我们还发现它的get方法返回的结果是由this.factory.getRepository()得到的,那这个this.factory又是谁呢,这时我们就要返回来看afterPropertiesSet方法的第一行代码:

this.factory = createRepositoryFactory();

直接看createRepositoryFactory方法,它是在JpaRepositoryFactoryBean的直接父类TransactionalRepositoryFactoryBeanSupport中实现的。

protected final RepositoryFactorySupport createRepositoryFactory() {

    RepositoryFactorySupport factory = doCreateRepositoryFactory();

    RepositoryProxyPostProcessor exceptionPostProcessor = this.exceptionPostProcessor;

    if (exceptionPostProcessor != null) {
        factory.addRepositoryProxyPostProcessor(exceptionPostProcessor);
    }

    RepositoryProxyPostProcessor txPostProcessor = this.txPostProcessor;

    if (txPostProcessor != null) {
        factory.addRepositoryProxyPostProcessor(txPostProcessor);
    }

    return factory;
}

我们直接看关键代码 – doCreateRepositoryFactory方法,这个是由JpaRepositoryFactoryBean类自己实现的。

protected RepositoryFactorySupport doCreateRepositoryFactory() {

    Assert.state(entityManager != null, "EntityManager must not be null!");

    return createRepositoryFactory(entityManager);
}

/**
 * Returns a {@link RepositoryFactorySupport}.
 */
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {

    JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager);
    jpaRepositoryFactory.setEntityPathResolver(entityPathResolver);
    jpaRepositoryFactory.setEscapeCharacter(escapeCharacter);

    if (queryMethodFactory != null) {
        jpaRepositoryFactory.setQueryMethodFactory(queryMethodFactory);
    }

    return jpaRepositoryFactory;
}

我们直接看到createRepositoryFactory方法,它最后返回是一个JpaRepositoryFactory类型的对象,其他的我们就先不关心了,直接看JpaRepositoryFactory类。我们先来一张类关系图:
在这里插入图片描述
我们直奔我们的主题-- getRepository方法,它是在JpaRepositoryFactory的父类RepositoryFactorySupport中实现的。

public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {

    ...
        
    RepositoryInformation information = getRepositoryInformation(metadata, composition);

    ...
        
    Object target = getTargetRepository(information);

    ...
        
    ProxyFactory result = new ProxyFactory();
    result.setTarget(target);
    result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);

    ...
        
    T repository = (T) result.getProxy(classLoader);

    ...

    return repository;
}

实现代码有点多,这里仅展示我们关注的关键代码,去掉其他多余的代码之后,有没有发现上面的逻辑很熟悉,其实就是构建一个代理类实例的过程,因此也解释了为什么我们在第一次调试看到实际注入的是一个JdkDynamicAopProxy类型的实体。那我们得好好看看代理对象的目标对象是怎么得到的,请看getTargetRepository方法,发现他需要一个RepositoryInformation类型的传参,我继续往上找,看到了getRepositoryInformation方法。

private RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata,
        RepositoryComposition composition) {

    RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, composition);

    return repositoryInformationCache.computeIfAbsent(cacheKey, key -> {

        Class<?> baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata));

        return new DefaultRepositoryInformation(metadata, baseClass, composition);
    });
}

其中,可以看到一个getRepositoryBaseClass方法

protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
    return SimpleJpaRepository.class;
}

好了,什么也不用说了,它直接给我们返回了SimpleJpaRepository的Class对象。其他的逻辑我们也不看了,虽然还有一些包装和判断的过程,但是我们今天的目的已经达到了,第二个疑惑也算比较好的解答了。最后顺便提一句,如果你的项目是手动使用了@EnableJpaRepositories注解,可能你的调试过程开局会有点不一样,但是后续的逻辑是相同的,自己可以去试试。代码如下:

@EnableJpaRepositories(basePackages = "ai.advance.jpademo.repository")
@SpringBootApplication
public class JpaDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(JpaDemoApplication.class, args);
    }

}

3.3 自定义查询

我们还有一个疑问,为什么我们自己定义的那些不属于SimpleJpaRepository类的方法也能被调用,并且被正确生成SQL?我们又要回到上节分析的getRepository方法,同样的我们去掉不需要关心的代码。

public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {
    
    ...
        
    ProxyFactory result = new ProxyFactory();
    
    ...
        
    Optional<QueryLookupStrategy> queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
            evaluationContextProvider);
    result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy,
            namedQueries, queryPostProcessors, methodInvocationListeners));

    result.addAdvice(
            new ImplementationMethodExecutionInterceptor(information, compositionToUse, methodInvocationListeners));

    T repository = (T) result.getProxy(classLoader);
    
    ...
        
    return repository;
}

从上面的源码我们可以看到ProxyFactory对象在最后有两次addAdvice方法的调用,目前是为了增加QueryExecutorMethodInterceptor和ImplementationMethodExecutionInterceptor两个拦截器,它们都实现了MethodInterceptor接口,其中ImplementationMethodExecutionInterceptor为RepositoryFactorySupport的静态内部类。

3.3.1 QueryExecutorMethodInterceptor

QueryExecutorMethodInterceptor这个拦截器是用来拦截处理我们在repository接口中自定义的方法的。在QueryExecutorMethodInterceptor的成员变量中有一个定义为Map<Method, RepositoryQuery>类型的queries变量,这个变量主要保存了自定义方法对象与一个RepositoryQuery对象的映射关系。
在这里插入图片描述
RepositoryQuery的直接抽象子类是AbstractJpaQuery,可以看到,一个RepositoryQuery实例持有一个JpaQueryMethod实例,JpaQueryMethod又持有一个Method实例,所以RepositoryQuery实例的用途很明显,一个RepositoryQuery代表了Repository接口中的一个方法,根据方法头上注解不同的形态,将每个Repository接口中的方法分别映射成相对应的RepositoryQuery实例。我们通过类关系图来熟悉一下RepositoryQuery具体的实现类有哪些。
在这里插入图片描述
下面我们看看JPA在哪些情况下创建对应的那个RepositoryQuery对象

  1. SimpleJpaQuery

方法头上@Query注解的nativeQuery属性缺省值为false,也就是使用JPQL,此时会创建SimpleJpaQuery实例,并通过两个StringQuery类实例分别持有query JPQL语句和根据query JPQL计算拼接出来的countQuery JPQL语句

  1. NativeJpaQuery

方法头上@Query注解的nativeQuery属性如果显式的设置为nativeQuery=true,也就是使用原生SQL的时候

  1. PartTreeJpaQuery

方法头上未进行@Query注解,将使用spring-data-jpa独创的方法名识别的方式进行sql语句拼接

  1. NamedQuery

使用javax.persistence.NamedQuery注解访问数据库的形式的时候

  1. StoredProcedureJpaQuery

在Repository接口的方法头上使用org.springframework.data.jpa.repository.query.Procedure注解,也就是调用存储过程的方式访问数据库的时候

所以QueryExecutorMethodInterceptor最终的目的就是根据当前需要调用的自定义的Repository的方法找到对应的RepositoryQuery对象,并构建调用信息并使用invoke方法触发调用,主要逻辑在QueryExecutorMethodInterceptor类的doInvoke方法中。

private Object doInvoke(MethodInvocation invocation) throws Throwable {

    Method method = invocation.getMethod();

    if (hasQueryFor(method)) {

        RepositoryMethodInvoker invocationMetadata = invocationMetadataCache.get(method);

        if (invocationMetadata == null) {
            invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(method, queries.get(method));
            invocationMetadataCache.put(method, invocationMetadata);
        }

        return invocationMetadata.invoke(repositoryInformation.getRepositoryInterface(), invocationMulticaster,
                invocation.getArguments());
    }

    return invocation.proceed();
}

其实最终调用的是RepositoryMethodInvoker类中的doInvoke方法,我们打上断点来看一下
在这里插入图片描述

3.3.2 ImplementationMethodExecutionInterceptor

ImplementationMethodExecutionInterceptor这个拦截器是来处理SimpleJpaRepository类本身实现的方法调用的。它是RepositoryFactorySupport类的静态内部类,只要没有被QueryExecutorMethodInterceptor拦截器处理的方法调用都会由它来处理,最终也是调用invoke方法。

public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {

    Method method = invocation.getMethod();
    Object[] arguments = invocation.getArguments();

    try {
        return composition.invoke(invocationMulticaster, method, arguments);
    } catch (Exception e) {
        org.springframework.data.repository.util.ClassUtils.unwrapReflectionException(e);
    }

    throw new IllegalStateException("Should not occur!");
}

其实这里面的逻辑属于中规中矩的代理类的拦截调用。我们拿findById这个方法作为例子,打上断点看看
在这里插入图片描述
从上面信息可以看出来ImplementationMethodExecutionInterceptor类的invoke方法调用的是RepositoryComposition类的invoke方法,如果继续深入,其实最终也是调用的RepositoryMethodInvoker类中的doInvoke方法
在这里插入图片描述
至此,我们开头的几大疑惑基本都得到了解释。

结束语

我也是尽量把自己知道的知识写明白,奈何本人水平有限,接触JPA也不久,如果存在纰漏欢迎指正。关于JPA更深层次的东西我也还在学习中,希望之后有机会再分享。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JPAJava Persistence API的缩写,是Java EE规范中用于ORM(对象关系映射)的API。它定义了一组接口和注解,使开发人员可以通过编写面向对象的代码来操作数据库。引用提到了在pom.xml中添加了两个依赖,即org.springframework.data:spring-data-jpa和org.springframework.boot:spring-boot-starter-data-jpa,这是使用Spring Data JPA时需要添加的依赖。 Spring Data JPA是在JPA规范下对Repository层进行封装的实现。它提供了一套简化的方法和规范,使开发人员可以更轻松地进行数据库操作。引用中的代码片段展示了如何定义一个符合Spring Data JPA规范的DAO层接口。通过继承JpaRepository和JpaSpecificationExecutor接口,我们可以获得封装了基本CRUD操作和复杂查询的功能。 关于JPASpring Data JPA的区别,引用提到了一个很好的解释。JPA是一种规范,而Spring Data JPA是在JPA规范下提供的Repository层的实现。通过使用Spring Data JPA,我们可以方便地在不同的ORM框架之间进行切换,而不需要更改代码。Spring Data JPA还对Repository层进行了封装,省去了开发人员的不少麻烦。 综上所述,JPAJava EE规范中的API,而Spring Data JPA是在JPA规范下的Repository层的实现。Spring Data JPA封装了JPA规范,提供了更方便的方法和规范,使开发人员可以更轻松地进行数据库操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [JPASpring-Data-JPA简介](https://blog.csdn.net/benjaminlee1/article/details/53087351)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *2* [JPA & Spring Data JPA详解](https://blog.csdn.net/cd546566850/article/details/107180272)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值