引言
問題起源於對新專案-數字核心的程式碼審查,在審閱賬戶模組後,發現補錄、更新等介面沒有呼叫JPA
的倉庫save
方法進行資料持久化,但更新依然生效,隨查閱資料文獻,開啟了對本議題的探究。
目標:沒有呼叫save
方法,更新是怎麼生效的?
試一試
在查閱大量資料後,瞭解到與JPA
持久化上下文Persistence Context
有關,一起試試吧。
實驗準備
初始化spring-boot
專案,依賴spring-data-jpa
,並開啟spring-data
的show-sql
配置以便除錯。
spring:
jpa:
show-sql: true
建立客戶資訊實體:
/**
* 客戶資訊表
*/
@Entity
@Table(name = "CUSTOMER")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 客戶姓名
*/
private String name;
/**
* 客戶手機號
*/
private String phone;
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
", phone='" + phone + '\'' +
'}';
}
}
配置DataJpaTest
啟用JPA
測試環境,不啟動整個spring-context
,可以減少單元測試執行耗時。
/**
* 客戶資訊倉庫測試
*/
@DataJpaTest // 自動配置JPA測試
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 不啟用內嵌資料庫替代
public class CustomerRepositoryTest {
private static final Logger logger = LoggerFactory.getLogger(CustomerRepositoryTest.class);
}
復現更新場景
更新方法如下,構建一條Hello Kitty!
測試資料,並對儲存後的實體資訊進行修改,但不呼叫save
。
/**
* 資料更新方法
*/
public void update() {
logger.info("----------更新測試用例開始----------");
Customer customer = new Customer();
customer.setName("Hello Kitty!");
customer.setPhone("17712345678");
this.customerRepository.save(customer);
logger.info("構建測試資料: {}", customer);
Long id = customer.getId();
this.customerRepository.findById(id).ifPresent(entity -> {
entity.setName("Hello 冬泳怪鴿!");
entity.setPhone("18888888888");
logger.info("更新測試資料: {}", entity);
});
logger.info("----------更新測試用例結束----------");
}
開啟事務,設定事務不回滾,呼叫上文的update
方法。
@Test
@Transactional // 開啟事務
@Rollback(value = false) // 事務不回滾
public void updateCustomerInTransaction() {
this.update();
}
檢視資料庫,更新成功。
檢視日誌,在updateCustomerInTransaction
方法執行完後,Hibernate
執行了update CUSTOMER set name=?, phone=? where id=?
更新,自動更新成功。
2022-01-15 09:42:34.461 INFO 8206 --- [ main] c.s.q.s.r.CustomerRepositoryTest : ----------更新測試用例開始----------
Hibernate: insert into CUSTOMER (name, phone) values (?, ?)
2022-01-15 09:42:34.537 INFO 8206 --- [ main] c.s.q.s.r.CustomerRepositoryTest : 構建測試資料: Customer{id=1, name='Hello Kitty!', phone='17712345678'}
2022-01-15 09:42:34.559 INFO 8206 --- [ main] c.s.q.s.r.CustomerRepositoryTest : 更新測試資料: Customer{id=1, name='Hello 冬泳怪鴿!', phone='18888888888'}
2022-01-15 09:42:34.559 INFO 8206 --- [ main] c.s.q.s.r.CustomerRepositoryTest : ----------更新測試用例結束----------
Hibernate: update CUSTOMER set name=?, phone=? where id=?
關閉事務,對比實驗。
@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED) // 掛起/關閉事務
public void updateCustomerWithoutTransaction() {
this.update();
}
檢視資料庫,資料沒有更新。
檢視日誌,Hibernate
沒有執行update
自動更新。
2022-01-15 10:26:20.866 INFO 8897 --- [ main] c.s.q.s.r.CustomerRepositoryTest : ----------更新測試用例開始----------
Hibernate: insert into CUSTOMER (name, phone) values (?, ?)
2022-01-15 10:26:20.996 INFO 8897 --- [ main] c.s.q.s.r.CustomerRepositoryTest : 構建測試資料: Customer{id=2, name='Hello Kitty!', phone='17712345678'}
Hibernate: select customer0_.id as id1_0_0_, customer0_.name as name2_0_0_, customer0_.phone as phone3_0_0_ from CUSTOMER customer0_ where customer0_.id=?
2022-01-15 10:26:21.054 INFO 8897 --- [ main] c.s.q.s.r.CustomerRepositoryTest : 更新測試資料: Customer{id=2, name='Hello 冬泳怪鴿!', phone='18888888888'}
2022-01-15 10:26:21.054 INFO 8897 --- [ main] c.s.q.s.r.CustomerRepositoryTest : ----------更新測試用例結束----------
對比實驗結果:事務開啟的前提下,對實體的改動會自動持久化到資料庫;當事務關閉時,則不生效。
持久化上下文
我們先來了解下JPA
持久化上下文:
The persistence context is the first-level cache where all the entities are fetched from the database or saved to the database. It sits between our application and persistent storage.
Persistence context keeps track of any changes made into a managed entity. If anything changes during a transaction, then the entity is marked as dirty. When the transaction completes, these changes are flushed into persistent storage.
If every change made in the entity makes a call to persistent storage, we can imagine how many calls will be made. This will lead to a performance impact because persistent storage calls are expensive.持久化上下文是一級快取,快取中所有實體都是從資料庫中
fetch
或save
到資料庫中的,它位於應用程式和持久儲存之間。
持久化上下文跟蹤所管理實體的所有更改,如果在事務中發生改變,實體會被標記為dirty
,事務完成後,所有改動會同步到持久儲存。
如果實體做的每一次改動都要呼叫儲存,可以想象需要將呼叫很多次,這會引起效能問題,因為持久儲存呼叫很昂貴。
具體分析下事務狀態下實驗的日誌:
①2022-01-15 09:42:34.461 INFO 8206 --- [ main] c.s.q.s.r.CustomerRepositoryTest : ----------更新測試用例開始----------
②Hibernate: insert into CUSTOMER (name, phone) values (?, ?)
③2022-01-15 09:42:34.537 INFO 8206 --- [ main] c.s.q.s.r.CustomerRepositoryTest : 構建測試資料: Customer{id=1, name='Hello Kitty!', phone='17712345678'}
④2022-01-15 09:42:34.559 INFO 8206 --- [ main] c.s.q.s.r.CustomerRepositoryTest : 更新測試資料: Customer{id=1, name='Hello 冬泳怪鴿!', phone='18888888888'}
⑤2022-01-15 09:42:34.559 INFO 8206 --- [ main] c.s.q.s.r.CustomerRepositoryTest : ----------更新測試用例結束----------
⑥Hibernate: update CUSTOMER set name=?, phone=? where id=?
- ① 方法開始
- ②
CUSTOMER
表插入資料,並將該實體儲存到持久化上下文 - ③ 列印持久化後的資料
- ④ 更新實體資料,這裡沒有執行
select
語句,因為持久化上下文存在該實體,findById
直接從持久化上下文中獲取 - ⑤ 方法結束
- ⑥ 事務提交前,檢查實體為
Dirty
,改動同步到資料庫
強制更新實驗
如果在JPA
持久化上下文中強制呼叫save
會發生什麼?
修改更新方法為強制更新,在修改entity
後手動呼叫save
方法更新。
/**
* 資料強制更新方法
*/
public void forceUpdate() {
logger.info("----------強制更新測試用例開始----------");
Customer customer = new Customer();
customer.setName("Hello Kitty!");
customer.setPhone("17712345678");
this.customerRepository.save(customer);
logger.info("構建測試資料: {}", customer);
Long id = customer.getId();
this.customerRepository.findById(id).ifPresent(entity -> {
entity.setName("Hello 冬泳怪鴿!");
entity.setPhone("18888888888");
this.customerRepository.save(entity);
logger.info("更新測試資料: {}", entity);
});
logger.info("----------強制更新測試用例結束----------");
}
開啟事務,設定事務不回滾,呼叫上文的forceUpdate
方法。
@Test
@Transactional // 開啟事務
@Rollback(value = false) // 事務不回滾
public void forceUpdateCustomerInTransaction() {
this.forceUpdate();
}
日誌執行結果如下,強制呼叫了save
方法,但非立即執行,最終的update
語句仍在方法結束後執行。
2022-01-15 11:38:52.810 INFO 9512 --- [ main] c.s.q.s.r.CustomerRepositoryTest : ----------強制更新測試用例開始----------
Hibernate: insert into CUSTOMER (name, phone) values (?, ?)
2022-01-15 11:38:52.914 INFO 9512 --- [ main] c.s.q.s.r.CustomerRepositoryTest : 構建測試資料: Customer{id=3, name='Hello Kitty!', phone='17712345678'}
2022-01-15 11:38:52.943 INFO 9512 --- [ main] c.s.q.s.r.CustomerRepositoryTest : 更新測試資料: Customer{id=3, name='Hello 冬泳怪鴿!', phone='18888888888'}
2022-01-15 11:38:52.943 INFO 9512 --- [ main] c.s.q.s.r.CustomerRepositoryTest : ----------強制更新測試用例結束----------
Hibernate: update CUSTOMER set name=?, phone=? where id=?
關閉事務,對比實驗。
@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED) // 掛起/關閉事務
public void forceUpdateCustomerWithoutTransaction() {
this.forceUpdate();
}
日誌如下,多執行了一個select + update
,猜測JPA
不開啟事務的情況下,先查詢當前實體資訊和資料庫記錄是否有變化,有變化則進行更新。
2022-01-15 12:29:27.616 INFO 9977 --- [ main] c.s.q.s.r.CustomerRepositoryTest : ----------強制更新測試用例開始----------
Hibernate: insert into CUSTOMER (name, phone) values (?, ?)
2022-01-15 12:29:27.721 INFO 9977 --- [ main] c.s.q.s.r.CustomerRepositoryTest : 構建測試資料: Customer{id=4, name='Hello Kitty!', phone='17712345678'}
Hibernate: select customer0_.id as id1_0_0_, customer0_.name as name2_0_0_, customer0_.phone as phone3_0_0_ from CUSTOMER customer0_ where customer0_.id=?
Hibernate: select customer0_.id as id1_0_0_, customer0_.name as name2_0_0_, customer0_.phone as phone3_0_0_ from CUSTOMER customer0_ where customer0_.id=?
Hibernate: update CUSTOMER set name=?, phone=? where id=?
2022-01-15 12:29:27.801 INFO 9977 --- [ main] c.s.q.s.r.CustomerRepositoryTest : 更新測試資料: Customer{id=4, name='Hello 冬泳怪鴿!', phone='18888888888'}
2022-01-15 12:29:27.801 INFO 9977 --- [ main] c.s.q.s.r.CustomerRepositoryTest : ----------強制更新測試用例結束----------
總結
- 開啟事務,
JPA
持久化上下文在事務提交時進行實體髒檢查,並同步到資料庫。 JPA
持久化上下文作為應用程式和資料庫之前的一級快取,減少對儲存的呼叫,提升效能。