JPA 實體髒檢查與儲存同步(Dirty & Flush)

張喜碩發表於2022-01-15

引言

問題起源於對新專案-數字核心的程式碼審查,在審閱賬戶模組後,發現補錄、更新等介面沒有呼叫JPA的倉庫save方法進行資料持久化,但更新依然生效,隨查閱資料文獻,開啟了對本議題的探究。

目標:沒有呼叫save方法,更新是怎麼生效的?

試一試

在查閱大量資料後,瞭解到與JPA持久化上下文Persistence Context有關,一起試試吧。

實驗準備

初始化spring-boot專案,依賴spring-data-jpa,並開啟spring-datashow-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();
}

檢視資料庫,更新成功。

image.png

檢視日誌,在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();
}

檢視資料庫,資料沒有更新。

image.png

檢視日誌,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.

持久化上下文是一級快取,快取中所有實體都是從資料庫中fetchsave到資料庫中的,它位於應用程式和持久儲存之間。
持久化上下文跟蹤所管理實體的所有更改,如果在事務中發生改變,實體會被標記為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         : ----------強制更新測試用例結束----------

總結

  1. 開啟事務,JPA持久化上下文在事務提交時進行實體髒檢查,並同步到資料庫。
  2. JPA持久化上下文作為應用程式和資料庫之前的一級快取,減少對儲存的呼叫,提升效能。

相關文章