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()
方法。该方法会:
- 检查缓存和持久化上下文
- 由于对象是新的,找不到对应记录
- 尝试从数据源加载实体
此时日志中会出现 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 项目。