51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Data JPA 执行 INSERT 时跳过 SELECT

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

赞(4)
未经允许不得转载:工具盒子 » Spring Data JPA 执行 INSERT 时跳过 SELECT