开篇词
从我们进入编程的世界,成为程序员到现在为止,总有几个感觉神奇和激动的时刻,其中肯定包括你第一次程序连上数据库可以实现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
- 在查询场景中,自定义的查询接口中,find关键词(也可以是search、query、get)后面必须跟随By关键词
- Between适用于数值、日期字段,用于日期时,参数类型可以是java.util.Date或java.sql.Timestamp
List<Film> findByLengthBetween(Integer low, Integer up);
List<Film> findByLastUpdateBetween(Date startDate, Date endDate);
- IsEmpty / IsNotEmpty只能用于集合类型的字段
- Before或者After可用于日期、数值类型的字段
List<Film> findByLengthBefore(Integer length)
- 涉及到删除和修改时,需要在方法定义上加上@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需要注意的地方
- 如果接口方法返回的是实体对象,如List、Actor这样的,则SELECT部分不能指定字段名,必须为*
- 如果只想查询指定列的数据,方法定义时的返回类型可以是Map<String, Object>、String[],返回数据如果是多条,则用集合类嵌套
- 关于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中常用的注解
- @Entity(name=“xxx”)
表明本类是一个JPA实体,name的默认值就是类名。
- @Table(name=“xxx”)
指定和数据库中映射表的名称,如果表名的驼峰式写法与类名相同,这可以省略name的值
- @Id
用于表示该属性作为ID主键
- @GeneratedValue(strategy=GenerationType.AUTO)
用于指定主键生成策略,与@Id注解配合使用。GenerationType总共有四个:
- AUTO:表示主键自增长由实现jpa的框架来控制
- TABLE:由一个表来维护主键,这个表记录上一次生成的主键,然后+1给作为新的主键,这种方式效率比较低
- SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列
- IDENTITY:主键增长由数据库来维护,可能不同数据库有不同的策略(主要是自动增长型)
- @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表示小数点所占的位数。
- @Enumerated(EmumType.STRING)
用于标识该枚举属性对应的数据库字段存储的是ordinal值还是name值。在后面的小节我会对枚举字段做更详细的介绍。EmumType总共有两个值:
- STRING:表示存储的是枚举的name值
- ORDINAL:表示存储的是枚举的ordinal值
- @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对象
- SimpleJpaQuery
方法头上@Query注解的nativeQuery属性缺省值为false,也就是使用JPQL,此时会创建SimpleJpaQuery实例,并通过两个StringQuery类实例分别持有query JPQL语句和根据query JPQL计算拼接出来的countQuery JPQL语句
- NativeJpaQuery
方法头上@Query注解的nativeQuery属性如果显式的设置为nativeQuery=true,也就是使用原生SQL的时候
- PartTreeJpaQuery
方法头上未进行@Query注解,将使用spring-data-jpa独创的方法名识别的方式进行sql语句拼接
- NamedQuery
使用javax.persistence.NamedQuery注解访问数据库的形式的时候
- 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更深层次的东西我也还在学习中,希望之后有机会再分享。