1、概览 {#1概览}
在某些情况下,当使用 Spring Data JPA Repository 保存实体时,可能会在日志中遇到额外的 SELECT
。这可能会因大量额外调用而导致性能问题。
本文将带你了解如何在 Spring Data JPA 中执行 INSERT
时跳过 SELECT
,以提高性能。
2、设置 {#2设置}
在深入 Spring Data JPA 并对其进行测试之前,先要做一些准备工作。
2.1、依赖 {#21依赖}
为了创建测试 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、配置 {#22配置}
本例中使用的 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 查询的原因 {#3导致-select-查询的原因}
首先,创建一个 Task
实体:
@Entity
public class Task {
@Id
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 {#4使用-generatedvalue}
可能的解决方案之一是不在应用端指定 ID。可以使用 @GeneratedValue
注解,并指定用于在数据库端生成 ID 的策略。
创建 TaskWithGeneratedId
实体,为 ID 指定生成策略:
@Entity
public class TaskWithGeneratedId {
@Id
@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 接口 {#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;
}
// 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() 方法 {#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 {#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 注解方法 {#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 缓存的使用。
9、总结 {#9总结}
本文介绍了在使用 Spring Data JPA 保存(INSERT
)实体时生成额外 SELECT
查询的原因,以及如何避免这个问题。
Ref:https://www.baeldung.com/spring-data-jpa-skip-select-insert