1. 概述

在使用 Spring Data JPA Repository 保存实体时,我们有时会在日志中发现额外的 SELECT 查询。这些不必要的调用可能导致性能问题,尤其是在高频操作场景下。

本文将探讨几种跳过额外 SELECT 查询的方法,从而提升应用性能

2. 环境准备

在深入 Spring Data JPA 测试前,需要完成以下准备工作。

2.1 依赖配置

创建测试仓库需添加 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 测试上下文:

<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 自动生成数据库结构并打印所有 SQL 语句。

3. 额外 SELECT 查询的成因

首先创建实体类:

@Entity
public class Task {
    @Id
    private Integer id;
    private String description;
    // getters and setters
}

创建对应的 Repository:

@Repository
public interface TaskRepository extends JpaRepository<Task, Integer> {
}

当保存指定 ID 的新实体时:

@Autowired
private TaskRepository taskRepository;

@Test
void givenRepository_whenSaveNewTaskWithPopulatedId_thenExtraSelectIsExpected() {
    Task task = new Task();
    task.setId(1);
    taskRepository.saveAndFlush(task);
}

调用 saveAndFlush()save() 行为相同)时,内部执行逻辑如下:

public<S extends T> S save(S entity){
    if(isNew(entity)){
        entityManager.persist(entity);
        return entity;
    } else {
        return entityManager.merge(entity);
    }
}

当实体被视为非新对象时,会调用 merge() 方法。该方法会:

  1. 检查缓存和持久化上下文
  2. 由于对象是新的,找不到对应记录
  3. 尝试从数据源加载实体

此时日志中会出现 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, ?)

当在应用层指定 ID 时,JPA 会将其视为非新对象,导致额外的 SELECT 查询

4. 使用 @GeneratedValue 注解

解决方案之一:不在应用层指定 ID。通过 @GeneratedValue 注解让数据库生成 ID。

修改实体类:

@Entity
public class TaskWithGeneratedId {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
}

保存时不设置 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> {
    @Id
    private int id;

    @Transient
    private boolean isNew = true;

    @Override
    public Integer getId() {
        return id;
    }

    @Override
    public boolean isNew() {
        return isNew;
    }
    // getters and setters
}

添加 @Transient 标记的 isNew 字段(不创建数据库列)。通过重写 isNew() 方法,即使指定 ID 也可将实体视为新对象。

JPA 内部判断逻辑变为:

public class JpaPersistableEntityInformation {
    public boolean isNew(T entity) {
        return entity.isNew();
    }
}

保存实体测试:

@Test
void givenRepository_whenSaveNewPersistableTask_thenNoExtraSelectIsExpected() {
    PersistableTask task = new PersistableTask();
    task.setId(2);
    task.setNew(true);
    persistableTaskRepository.saveAndFlush(task);
}

日志仅显示 INSERT 语句,且实体保留了指定 ID。

⚠️ 注意:保存相同 ID 的多个实体会抛出异常:

@Test
void givenRepository_whenSaveNewPersistableTasksWithSameId_thenExceptionIsExpected() {
    PersistableTask task = new PersistableTask();
    task.setId(3);
    task.setNew(true);
    persistableTaskRepository.saveAndFlush(task);

    PersistableTask duplicateTask = new PersistableTask();
    duplicateTask.setId(3);
    duplicateTask.setNew(true);

    assertThrows(DataIntegrityViolationException.class,
      () -> persistableTaskRepository.saveAndFlush(duplicateTask));
}

当自行生成 ID 时,需确保其唯一性

6. 直接使用 persist() 方法

前述方案最终都调用了 persist() 方法。我们可以扩展 Repository 直接调用此方法

创建扩展接口:

public interface TaskRepositoryExtension {
    Task persistAndFlush(Task task);
}

实现该接口:

@Component
public class TaskRepositoryExtensionImpl implements TaskRepositoryExtension {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Task persistAndFlush(Task task) {
        entityManager.persist(task);
        entityManager.flush();
        return task;
    }
}

扩展原 Repository:

@Repository
public interface TaskRepository extends JpaRepository<Task, Integer>, TaskRepositoryExtension {
}

调用自定义方法保存:

@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 Utils 的 BaseJpaRepository

上一节思路已在 Hypersistence Utils 项目中实现。该项目提供 BaseJpaRepository,包含 persistAndFlush() 及其批量操作方法。

添加依赖(根据 Hibernate 版本选择):

<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-55</artifactId>
</dependency>

创建混合 Repository:

@Repository
public interface TaskJpaRepository extends JpaRepository<Task, Integer>, BaseJpaRepository<Task, Integer> {
}

启用 BaseJpaRepository 实现:

@EnableJpaRepositories(
    repositoryBaseClass = BaseJpaRepositoryImpl.class
)

保存实体测试:

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

日志中无 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 注解方法

通过原生查询直接插入数据

@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 从参数对象中获取。

保存测试:

@Test
void givenRepository_whenPersistNewTaskUsingNativeQuery_thenNoExtraSelectIsExpected() {
    Task task = new Task();
    task.setId(6);
    taskRepository.insert(task);

    assertTrue(taskRepository.findById(6).isPresent());
}

成功保存指定 ID 的实体,且无前置 SELECT 查询。

注意:此方案绕过了 JPA 上下文和 Hibernate 缓存。

9. 总结

在 Spring Data JPA 中由应用层生成 ID 时,可能出现额外的 SELECT 查询导致性能下降。本文探讨了多种解决方案:

数据库生成 ID:使用 @GeneratedValue
自定义实体状态:实现 Persistable 接口
直接持久化:扩展 Repository 调用 persist()
第三方库:Hypersistence Utils 的 BaseJpaRepository
原生查询:使用 @Query 注解

选择方案时需权衡:

  • 将 ID 生成逻辑移至数据库更简单
  • 自定义持久化逻辑需处理唯一性约束
  • 原生查询会绕过 ORM 优势

完整代码示例请参考 GitHub 项目


原始标题:Skip Select Before Insert in Spring Data JPA | Baeldung