在某些情况下,当使用
Spring Data JPA
Repository 保存实体时,可能会在日志中遇到额外的
SELECT
。这可能会因大量额外调用而导致性能问题。
本文将带你了解如何在 Spring Data JPA 中执行
INSERT
时跳过
SELECT
,以提高性能。
在深入 Spring Data JPA 并对其进行测试之前,先要做一些准备工作。
2.1、依赖
为了创建测试 Repository,需要使用
Spring Data JPA
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
使用
H2
数据库作为测试数据库:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
使用 Spring Context 进行集成测试。添加
spring-boot-starter-test
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.2、配置
本例中使用的 JPA 配置如下:
spring.jpa.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.hibernate.show_sql=true
spring.jpa.hibernate.hbm2ddl.auto=create-drop
如上,让 Hibernate 生成 schema,并将所有 SQL 查询记录到日志中。
3、导致 SELECT 查询的原因
首先,创建一个
Task
实体:
@Entity
public class Task {
private Integer id;
private String description;
// Getter/Setter 省略
为实体创建 Repository:
@Repository
public interface TaskRepository extends JpaRepository<Task, Integer> {
现在,保存一个新的
Task
,手动指定 ID:
@Autowired
private TaskRepository taskRepository;
@Test
void givenRepository_whenSaveNewTaskWithPopulatedId_thenExtraSelectIsExpected() {
Task task = new Task();
task.setId(1);
taskRepository.saveAndFlush(task);
当我们调用 Repository 的
saveAndFlush()
方法时和
save()
方法的行为将相同。在内部,使用以下代码:
public<S extends T> S save(S entity){
if(isNew(entity)){
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
因此,如果实体被认为不是新的,就会调用实体管理器的
merge()
方法。在
merge()
中,JPA 会检查实体是否存在于缓存和持久化上下文中。由于对象是新的,所以不会在那里找到。最后,它会尝试从数据源加载实体。
这就是我们在日志中遇到
SELECT
查询的地方。由于数据库中没有这个实体,因此在此之后调用
INSERT
保存:
Hibernate: select task0_.id as id1_1_0_, task0_.description as descript2_1_0_ from task task0_ where task0_.id=?
Hibernate: insert into task (id, description) values (default, ?)
在
isNew()
方法的实现中,可以找到如下代码:
public boolean isNew(T entity) {
ID id = this.getId(entity);
return id == null;
如果我们指定了 ID,实体将被视为新实体。在这种情况下,将会向数据库发起一个额外的
SELECT
查询。
4、使用 @GeneratedValue
可能的解决方案之一是不在应用端指定 ID。可以使用
@GeneratedValue
注解,并指定用于在数据库端生成 ID 的策略。
创建
TaskWithGeneratedId
实体,为 ID 指定生成策略:
@Entity
public class TaskWithGeneratedId {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
然后,保存一个
TaskWithGeneratedId
实体,但不设置 ID:
@Autowired
private TaskWithGeneratedIdRepository taskWithGeneratedIdRepository;
@Test
void givenRepository_whenSaveNewTaskWithGeneratedId_thenNoExtraSelectIsExpected() {
TaskWithGeneratedId task = new TaskWithGeneratedId();
TaskWithGeneratedId saved = taskWithGeneratedIdRepository.saveAndFlush(task);
assertNotNull(saved.getId());
可以从执行日志中看到,并没有
SELECT
查询,而且为实体生成了一个新的 ID。
5、实现 Persistable 接口
另一个选择是在实体中实现
Persistable
接口:
@Entity
public class PersistableTask implements Persistable<Integer> {
private int id;
@Transient
private boolean isNew = true;
@Override
public Integer getId() {
return id;
@Override
public boolean isNew() {
return isNew;
// Getter、Setter 省略
如上,添加了一个新字段
isNew
,并将其注解为
@Transient
,表示非数据表中的列。覆写
isNew()
方法,可以将实体视为新实体,即使指定了一个 ID。
现在,JPA 在底层使用另一种逻辑来判断实体是否是新的:
public class JpaPersistableEntityInformation {
public boolean isNew(T entity) {
return entity.isNew();
使用
PersistableTaskRepository
保存
PersistableTask
:
@Autowired
private PersistableTaskRepository persistableTaskRepository;
@Test
void givenRepository_whenSaveNewPersistableTask_thenNoExtraSelectIsExpected() {
PersistableTask persistableTask = new PersistableTask();
persistableTask.setId(2);
persistableTask.setNew(true);
PersistableTask saved = persistableTaskRepository.saveAndFlush(persistableTask);
assertEquals(2, saved.getId());
你可以看到,只有
INSERT
日志信息,实体中包含我们指定的 ID。
如果尝试保存几个具有相同 ID 的新实体,就会出现异常:
@Test
void givenRepository_whenSaveNewPersistableTasksWithSameId_thenExceptionIsExpected() {
PersistableTask persistableTask = new PersistableTask();
persistableTask.setId(3);
persistableTask.setNew(true);
persistableTaskRepository.saveAndFlush(persistableTask);
PersistableTask duplicateTask = new PersistableTask();
duplicateTask.setId(3);
duplicateTask.setNew(true);
assertThrows(DataIntegrityViolationException.class,
() -> persistableTaskRepository.saveAndFlush(duplicateTask));
因此,如果我们负责生成 ID,需要注意其唯一性。
6、直接使用 persist() 方法
如前例所示,所做的所有操作都会调用
persist()
方法。我们也可以为 Repository 创建一个扩展,允许我们直接调用该方法。
创建一个
TaskRepositoryExtension
接口,包含
persist
方法:
public interface TaskRepositoryExtension {
Task persistAndFlush(Task task);
然后,为这个接口创建一个实现 Bean:
@Component
public class TaskRepositoryExtensionImpl implements TaskRepositoryExtension {
@PersistenceContext
private EntityManager entityManager;
@Override
public Task persistAndFlush(Task task) {
entityManager.persist(task);
entityManager.flush();
return task;
现在,让
TaskRepository
继承此接口:
@Repository
public interface TaskRepository extends JpaRepository<Task, Integer>, TaskRepositoryExtension {
调用自定义的
persistAndFlush()
方法来保存
Task
实例:
@Test
void givenRepository_whenPersistNewTaskUsingCustomPersistMethod_thenNoExtraSelectIsExpected() {
Task task = new Task();
task.setId(4);
Task saved = taskRepository.persistAndFlush(task);
assertEquals(4, saved.getId());
你可以看到日志信息中只有
INSERT
调用,没有额外的
SELECT
调用。
7、使用 Hypersistence 中的 BaseJpaRepository
上一节的想法已经在
Hypersistence Utils
项目中实现。该项目提供了一个
BaseJpaRepository
,其中有
persistAndFlush()
方法的实现,以及它的批量版本
要使用它,必须添加额外的
依赖
(需要根据 Hibernate 版本选择正确的 Maven 构件):
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-55</artifactId>
</dependency>
现在实现另一个 Repository,它同时继承了
Hypersistence Utils
中的
BaseJpaRepository
和 Spring Data JPA 中的
JpaRepository
:
@Repository
public interface TaskJpaRepository extends JpaRepository<Task, Integer>, BaseJpaRepository<Task, Integer> {
此外,还必须使用
@EnableJpaRepositories
注解启用
BaseJpaRepository
的实现:
@EnableJpaRepositories(
repositoryBaseClass = BaseJpaRepositoryImpl.class
使用新 Repository 保存
Task
:
@Autowired
private TaskJpaRepository taskJpaRepository;
@Test
void givenRepository_whenPersistNewTaskUsingPersist_thenNoExtraSelectIsExpected() {
Task task = new Task();
task.setId(5);
Task saved = taskJpaRepository.persistAndFlush(task);
assertEquals(5, saved.getId());
成功保存了
Task
,而日志中没有
SELECT
查询。
与在应用端指定 ID 的所有示例一样,可能会出现违反唯一性约束的情况:
@Test
void givenRepository_whenPersistTaskWithTheSameId_thenExceptionIsExpected() {
Task task = new Task();
task.setId(5);
taskJpaRepository.persistAndFlush(task);
Task secondTask = new Task();
secondTask.setId(5);
assertThrows(DataIntegrityViolationException.class,
() -> taskJpaRepository.persistAndFlush(secondTask));
8、使用 @Query 注解方法
还可以通过直接使用本地查询来避免额外调用。在
TaskRepository
中添加如下方法:
@Repository
public interface TaskRepository extends JpaRepository<Task, Integer> {
@Modifying
@Query(value = "insert into task(id, description) values(:#{#task.id}, :#{#task.description})",
nativeQuery = true)
void insert(@Param("task") Task task);
该方法直接调用
INSERT
查询,避免了持久化上下文的工作。ID 将从方法参数中的
Task
对象中获取。
用该方法保存
Task
:
@Test
void givenRepository_whenPersistNewTaskUsingNativeQuery_thenNoExtraSelectIsExpected() {
Task task = new Task();
task.setId(6);
taskRepository.insert(task);
assertTrue(taskRepository.findById(6).isPresent());
成功使用 ID 保存了实体,无需在
INSERT
之前进行额外的
SELECT
查询。需要注意的是,使用这种方法可以避免 JPA 上下文和 Hibernate 缓存的使用。
本文介绍了在使用 Spring Data JPA 保存(
INSERT
)实体时生成额外
SELECT
查询的原因,以及如何避免这个问题。
Ref:
https://www.baeldung.com/spring-data-jpa-skip-select-insert
@Transactional 能和 @Async 一起用吗?
在 Spring Boot Filter 中获取响应体
Spring Boot 使用 git-commit-id-maven-plugin 打包应用
在 Spring Boot 中动态管理 Kafka Listener
使用 Key 和 SecretKey 签发 JWT Token