添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

在某些情况下,当使用 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
  •