在 Spring Data JPA 中插入之前跳過選擇
1. 概述
在某些情況下,當我們使用 Spring Data JPA 儲存庫來保存實體時,我們可能會在日誌中遇到額外的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 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 產生架構並將所有 SQL 查詢記錄到日誌中。
3. SELECT查詢的原因
讓我們看看為什麼我們有這樣額外的SELECT
查詢來實現簡單的儲存庫。
首先,讓我們建立一個實體:
@Entity
public class Task {
@Id
private Integer id;
private String description;
//getters and setters
}
現在,讓我們為該實體建立一個儲存庫:
@Repository
public interface TaskRepository extends JpaRepository<Task, Integer> {
}
現在,讓我們儲存一個指定 ID 的新Task
:
@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()
方法。在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 {
@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
我們的另一個選擇是在我們的實體中實作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
}
這裡我們新增了一個新字段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()
方法。我們還可以為我們的儲存庫建立一個擴展,允許我們直接呼叫此方法。
讓我們使用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 Utils 中的BaseJpaRepository
上一節的想法已在Hypersistence Utils專案中實現。該專案為我們提供了一個BaseJpaRepository
,其中我們有persistAndFlush()
方法實作及其批次模擬。
要使用它,我們必須指定額外的依賴項。我們應該根據 Hibernate 版本選擇正確的 Maven 工件:
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-55</artifactId>
</dependency>
讓我們實作另一個儲存庫,它擴展了 Hypersistence Utils 中的BaseJpaRepository
和 Spring Data JPA 中的JpaRepository
:
@Repository
public interface TaskJpaRepository extends JpaRepository<Task, Integer>, BaseJpaRepository<Task, Integer> {
}
另外,我們必須使用@EnableJpaRepositories
註解啟用BaseJpaRepository
的實作:
@EnableJpaRepositories(
repositoryBaseClass = BaseJpaRepositoryImpl.class
)
現在,讓我們使用新的儲存庫來儲存我們的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 快取。
9. 結論
當使用 Spring Data JPA 在應用程式端實作 ID 產生時,我們可能會遇到日誌中出現額外的SELECT
查詢,從而導致效能下降。在本文中,我們討論了解決此問題的各種策略。
在某些情況下,將此邏輯移至資料庫端或根據我們的需要微調持久化邏輯是有意義的。在做出決定之前,我們應該考慮每種策略的優點、缺點和潛在問題。
像往常一樣,完整的源代碼可以在 GitHub 上找到。