在 JPA 中正確使用 flush()
一、概述
在本教程中,我們將快速了解 Spring JPA 提供的flush()
方法。
首先,我們將學習涉及的關鍵抽象,包括Entit
Manager
和刷新模式。接下來,我們將使用Customer
和CustomerAddress
實體設置示例。然後,我們將編寫集成測試以查看flush()
如何與兩種刷新模式一起工作。我們將通過查看使用顯式flush().
2. 什麼是flush()
?
本質上flush()
方法是 JPA 中EntityManager
接口的一部分。 EntityManager
可用於與 JPA 中的持久性上下文交互。它提供了管理實體生命週期、查詢實體和對數據庫執行 CRUD 操作的方法。
flush()
方法用於同步對由持久性上下文管理的實體所做的任何更改與底層數據庫。當我們在EntityManager
上調用flush()
時,JPA 提供程序依次執行持久化或更新數據庫中的實體所需的任何 SQL 語句。
在深入探討該方法的正確使用之前,讓我們看一下與flush()
的工作密切相關的一個概念,即 JPA 提供的各種刷新模式。
JPA 中的刷新模式確定何時將對持久性上下文中的實體所做的更改與數據庫同步。 JPA 提供的兩種主要刷新模式是AUTO
和COMMIT
。
AUTO
是默認的沖洗模式。這意味著對實體所做的更改會在必要時自動與數據庫同步,例如提交事務時,或執行需要最新數據的查詢時。
另一方面, COMMIT
模式會延遲同步,直到事務被提交。這意味著在提交當前事務之前,對實體所做的更改對其他事務不可見。
3. 示例設置
讓我們考慮一個包含兩個實體Customer
和CustomerAddress
的簡單示例。 CustomerAddress
實體包含customer_id
一樣長。讓我們先定義Customer
實體:
@Entity
public class Customer {
private String name;
private int age;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
// getters and setters
}
接下來,讓我們定義Address
實體:
@Entity
public class CustomerAddress {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String street;
private String city;
private long customer_id;
// ... getters and setters
}
稍後,我們將使用這兩個類來測試可能需要flush()
各種場景。
3.1.設置EntityManager
在我們設置EntityManager
之前,我們需要獲取一個實現EntityManagerFactory
接口的類的實例:
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(dataSource);
emf.setPackagesToScan("com.baeldung.flush");
emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
emf.setJpaProperties(getHibernateProperties());
return emf;
}
對於要設置的EntityManagerFactory
,我們需要提供數據源和 JPA 屬性。
讓我們使用 H2 嵌入式數據庫作為我們的示例數據庫:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2)
.build();
接下來讓我們設置屬性:
Properties getHibernateProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.hbm2ddl.auto", "create");
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
return properties;
}
在後續部分中,我們將使用此EntityManagerFactory
來創建EntityManager.
我們將使用此EntityManager
來測試事務中使用和不使用flush()
的各種場景。
4. flush()
與FlushModeType.COMMIT
讓我們從將FlushModeType
設置為COMMIT
開始:
entityManager.setFlushMode(FlushModeType.COMMIT);
正如我們在開始時提到的, FlushModeType.COMMIT
延遲刷新對數據庫的更改,直到提交事務。這可以通過減少事務期間發送到數據庫的 SQL 語句的數量來提高性能,但如果其他事務修改相同的數據,也會增加數據不一致的風險。
要了解flush()
與EntityManager
類的persist()
結合使用的必要性,讓我們首先測試我們的Customer
創建並嘗試在不調用flush()
的情況下為新創建的Customer
查詢數據庫:
@Test
void givenANewCustomer_whenPersistAndNoFlush_thenDatabaseNotSynchronizedWithPersistentContextUsingCommitFlushMode() {
entityManager.setFlushMode(FlushModeType.COMMIT);
EntityTransaction transaction = getTransaction();
Customer customer = saveCustomerInPersistentContext("Alice", 30);
Customer customerInContext = entityManager.find(Customer.class, customer.getId());
assertDataInPersitentContext(customerInContext);
TypedQuery<Customer> retrievedCustomer = entityManager.createQuery("SELECT c FROM Customer c WHERE c.name = 'Alice'", Customer.class);
List<Customer> resultList = retrievedCustomer.getResultList();
assertThat(resultList).isEmpty();
transaction.rollback();
}
本質上,這裡我們使用之前配置的EntityManager
類型的 bean 來獲取Transaction
的實例:
EntityTransaction getTransaction() {
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
return transaction;
然後我們創建Customer
並使用entityManager.persist(customer)
持久化它:
Customer saveCustomerInPersistentContext(String name, int age) {
Customer customer = new Customer();
customer.setName(name);
customer.setAge(age);
entityManager.persist(customer);
return customer;
}
接下來,我們查詢數據庫以檢索新創建的 Customer 對象。
我們發現轉換為List<Customer>
的查詢結果是一個空列表,這意味著持久上下文中的任何內容都沒有與底層數據庫同步,因為事務尚未提交。
接下來,將FlushModeType
保持為COMMIT
,讓我們確保在實體管理器上調用persist()
之後調用flush()
:
@Test
void givenANewCustomer_whenPersistAndFlush_thenDatabaseSynchronizedWithPersistentContextUsingCommitFlushMode() {
entityManager.setFlushMode(FlushModeType.COMMIT);
EntityTransaction transaction = getTransaction();
Customer customer = saveCustomerInPersistentContext("Alice", 30);
entityManager.flush();
Long generatedCustomerID = customer.getId();
Customer customerInContext = entityManager.find(Customer.class, generatedCustomerID);
assertDataInPersitentContext(customerInContext);
TypedQuery<Customer> retrievedCustomer = entityManager.createQuery("SELECT c FROM Customer c WHERE c.name = 'Alice'", Customer.class);
Customer result = retrievedCustomer.getSingleResult();
assertThat(result).isEqualTo(EXPECTED_CUSTOMER);
transaction.rollback();
}
在這裡,我們可以看到通過在調用persist()
flush()
() 的調用,數據庫與持久上下文中的事務同步。我們將查詢數據庫並取回等於新創建的客戶實體的結果。重要的是,新的Customer
實體已經保存在數據庫中,即使FlushModeType
是COMMIT
並且事務本身尚未提交。
4.1.跨兩個實體查詢
此時,讓我們擴展我們的簡單示例,使其也包括CustomerAddress
實體。我們希望看到我們想要顯式調用涉及跨數據庫多個表的查詢的 f lush()
的潛在情況。
CustomerAddress
包含一個名為customer_id,
該字段在創建客戶實體後自動生成。我們可以在這裡看到我們想要保存一個Customer
,在數據庫中查詢id
並在CustomerAddress
實體中使用自動生成的id
:
@Test
public void givenANewCustomer_whenPersistAndFlush_thenCustomerIdGeneratedToBeAddedInAddress() {
entityManager.setFlushMode(FlushModeType.COMMIT);
EntityTransaction transaction = getTransaction();
saveCustomerInPersistentContext("John", 25);
entityManager.flush();
Customer retrievedCustomer = entityManager.createQuery("SELECT c FROM Customer c WHERE c.name = 'John'", Customer.class)
.getSingleResult();
Long customerId = retrievedCustomer.getId();
CustomerAddress address = new CustomerAddress();
address.setCustomer_id(customerId);
entityManager.persist(address);
entityManager.flush();
CustomerAddress customerAddress = entityManager.createQuery("SELECT a FROM CustomerAddress a WHERE a.customer_id = :customerID", CustomerAddress.class)
.setParameter("customerID", customerId)
.getSingleResult();
assertThat(customerAddress).isNotNull();
transaction.rollback();
}
最後,讓我們快速談談與flush()
方法相關的rollback()
的使用。本質上,當我們調用rollback()
時,事務中所做的任何更改都將被撤消。數據庫恢復到事務開始前的狀態。在我們上面測試的所有場景中,我們都在最後調用了rollback()
。這可以防止任何部分更改被提交到數據庫,這可能會導致數據不一致或損壞。
5. flush()
與FlushModeType.AUTO
接下來,讓我們看看當我們使用flush()
並將FlushModeType
設置為AUTO
時會發生什麼。
我們首先將沖洗模式設置為AUTO
:
entityManager.setFlushMode(FlushModeType.AUTO);
這裡 JPA 會自動檢測何時需要刷新並在執行查詢之前執行刷新。這確保了查詢結果是最新的,並且對持久對象所做的任何更改都保存在數據庫中。因此這裡不需要顯式調用flush()
:
@Test
void givenANewCustomer_whenPersistAndNoFlush_thenDBIsSynchronizedWithThePersistentContextWithAutoFlushMode() {
entityManager.setFlushMode(FlushModeType.AUTO);
EntityTransaction transaction = getTransaction();
Customer customer = saveCustomerInPersistentContext("Alice", 30);
Customer customerInContext = entityManager.find(Customer.class, customer.getId());
assertDataInPersitentContext(customerInContext);
TypedQuery<Customer> retrievedCustomer = entityManager.createQuery("SELECT c FROM Customer c WHERE c.name = 'Alice'", Customer.class);
Customer result = retrievedCustomer.getSingleResult();
assertThat(result).isEqualTo(EXPECTED_CUSTOMER);
transaction.rollback();
}
測試表明我們在 persist() 之後沒有調用任何flush()
。儘管如此,我們仍然可以看到查詢Customer
表會返回TypedQuery
由於FlushModeType
是AUTO
,因此不需要顯式flush()
將持久上下文中的實體與數據庫同步。
讓我們再次測試模式為AUTO
時的Customer
和CustomerAddress
創建場景:
@Test
public void givenFlushModeAutoAndNewCustomer_whenPersistAndNoFlush_thenCustomerIdGeneratedToBeAddedInAddress() {
entityManager.setFlushMode(FlushModeType.AUTO);
EntityTransaction transaction = getTransaction();
saveCustomerInPersistentContext("John", 25);
Customer singleResult = entityManager.createQuery("SELECT c FROM Customer c WHERE c.name = 'John'", Customer.class)
.getSingleResult();
Long customerId = singleResult.getId();
CustomerAddress address = new CustomerAddress();
address.setCustomer_id(customerId);
entityManager.persist(address);
CustomerAddress customerAddress = entityManager.createQuery("SELECT a FROM CustomerAddress a WHERE a.customer_id = :customerID", CustomerAddress.class)
.setParameter("customerID", customerId)
.getSingleResult();
assertThat(customerAddress).isNotNull();
transaction.rollback();
}
正如預期的那樣,這裡也使用AUTO
模式,不需要flush()
並且我們可以在事務仍在進行時創建CustomerAddress
之前在數據庫中查詢Customer
。
接下來,讓我們看看顯式使用flush()
的一些主要好處以及潛在問題。
6. 顯式flush()
的好處
以下是顯式flush()
的一些好處:
- 一致性:我們可以確保一旦調用
flush()
數據庫就與持久上下文同步,因此兩個組件之間具有實時一致性。 - 提高性能:如果使用得當,我們可以通過將相關更改分組到一個批次中來減少發送到數據庫的 SQL 語句的數量。這可以提高性能,尤其是當我們對同一組對象進行多次更改時。
- 即時反饋:當我們調用
flush()
時,任何未決的更改都會立即寫入數據庫。這可以提供有關操作成功或失敗的即時反饋,這將有助於調試和故障排除。 - 更好的資源管理:通過在對數據庫進行更改後立即將其刷新,我們可以減少在事務結束之前保留未決更改所需的內存和其他資源。
7. 顯式flush()
的潛在問題
但是,我們必須意識到不正確或頻繁調用flush()
可能導致的潛在問題:
- 數據庫爭用:使用
flush()
時,我們可能會面臨數據庫爭用。如果多個事務試圖訪問同一組數據,則最有可能發生這種情況。這可能會導致鎖定、死鎖和其他與數據庫相關的問題。 - 內存使用量增加:如果調用過於頻繁,可能會導致內存使用量增加,尤其是當我們對同一組對象進行多次更改時。這可能會導致內存不足錯誤和其他與內存相關的問題。
- 數據庫連接使用效率低下:如果過於頻繁地調用,
flush()
會導致數據庫連接使用效率低下。每次調用flush()
都需要與數據庫進行新的往返,這可能會導致連接池耗盡和其他與連接相關的問題。 - 數據完整性問題:
flush()
可能會導致數據完整性問題,特別是如果我們不注意更改的順序。這會導致數據庫中存儲的數據不一致和錯誤。
總的來說,考慮到對性能、內存使用和數據完整性的潛在影響,明智且僅在必要時使用flush()
非常重要。通過對相關更改進行分組並最大限度地減少發送到數據庫的 SQL 語句的數量來優化我們對flush()
的使用也很重要。
八、結論
在本文中,我們通過兩種刷新模式查看了*flush()*的正確用法。我們創建了各種集成測試,以了解如何在各種情況下正確使用flush()
。最後,我們討論了使用該方法的優缺點。
與往常一樣,示例代碼可在 GitHub 上獲得。