科技新知

Presist and Casecade

前次的文章,講了一些Spring Data最基本的關聯概念,但當要正式儲存或刪除,就有些考慮完整性問題。平常我們在處理資料庫的關聯表格時,也需要面Foreign Key的正確性問題。同樣地,Spring Data也有這方面的考量,但它有提份一個很方便的CascadeType選項,可以簡化一些流程。

假設你只能存取Parent Repo,那你需要在Parent中,加入CascadeType.All。當repo.save(parent)時,它就會順多把所有child的也一併進行Save,你也不需要有Child Repo的存在。

@OneToMany(mappedBy="parent", cascade = CascadeType.All)
List children = newArrayList<>();

但在複雜的狀況下,例如你不想在更新parent的情況下,不小心弄到child,特別是經過public web下的API操作,你對web client的資料正確性有存疑,就不要使用CascadeType了。這也是筆者認為在大多數情況下,我們都會把Parent和Child的CRUD分開操作,然後根據需要使用各自的repo save。

如果你一定要用CascadeType.ALL (CascadeType.REMOVE),就要再留意刪除的問題。為什麼?因為刪除 parent,其實指的是某個parent不再存在,但不代表child也要一起刪除,child的parent連結可以變為null,也有重新連結其他parent的可能。

如果大家確定需要共同刪除,就可以用CascadeType.ALL 或 CascadeType.REMOVE。

還有一個新的選擇,orphanRemoval = true,也有類似效果。

@OneToMany(mappedBy="parent", cascade = CascadeType.REMOVE)
List children = newArrayList<>();
// or@OneToMany(mappedBy="parent", orphanRemoval = true)
List children = newArrayList<>();
// or@OneToMany(mappedBy="parent", cascade = CascadeType.REMOVE, orphanRemoval = true)
List children = newArrayList<>();

筆者測試過,混著用也是可以的。若大家看過其他教程,可能會覺得orphanRemoval = true 和 CascadeType 總是一起出現,但它們其實是分別操作的。單獨使用orphanRemoval = true,有時候則是為了不會出現無主的child,但這不代表parent和child的想要同步更新。

JPA Entity 的生命週期

Spring Data跟傳統的資料庫Selete,Create,Update,Delete SQL 語句有所不同。也就是這個不同,它的CascadeType比資料庫的Cascade Update和Cascade Delete更強大。

Spring Data 預設其實是使用 jakarta.persistence.EntityManager,每個Entity主要分為四個狀態

  • Transient / New - 不在EntityManager的掌控中
  • Managed - 在EntityManager的掌控中,將會在下次flush時,變成sql create或update statement
  • Detached - 脫離EntityManager的掌控,不受flush影響
  • Removed - 在EntityManager的掌控中,將會在下次flush時,變成sql delete statement

在Spring Data / Jpa 以前,我們若要直接操作Hibernate,經常見到persist, remove的寫法

entityManager.persist(entity);
entityManager.remove(entity);

entityManager.detach(entity);
entityManager.merge(entity);

其實persist就是把處於Transient、Removed的entity,改為Managed。而remove就是把Managed改為Removed。detach,merge也類似,就是Managed,Detached之間互換。

EntityManager最強大的是,它可以讓程序員不需要再為Managed狀態下的entity操心,它會自動判別下次flush,應該create還是update,如果完全沒有改動的,連update也不會執行。

(註,flush和commit也有不同,flush就是從java寫到資料庫中,在資料庫commit前,還可以使用rollback放棄。)

而Spring Data,則是進一步簡化,它把persist改為save,remove改為delete,然後自動選擇flush的時機。

CascadeType

在解釋完Entity 的生命週期後,終於可以回到CascadeType了。這裏的CascadeType不是資料庫的Cascade操作,其實它是指EntityManager的狀態操作是否有傳遞關係。亦即是,persist(parent)時,要不要連同child也一起操作?

我們查看 CasecadeType 的原始碼,就可以發現可以被傳遞的操作共有以下這些

  • PERSIST
  • MERGE
  • REMOVE
  • REFRESH
  • DETACH
  • ALL (以上全部)

這裏的 CasecadeType.PERSIST ,跟資料庫的 Cascade Update 是不一樣的。資料庫裏的 Cascade Update,是指當 Parent 的 Primary Key 有變,對應child的 Foreign Key也一起變。但因為 JPA Entity 的機制, Parent 的 Primary Key 不可以改變,理論上不會發生類似資料庫的 Cascade Update,頂多有 Cascade Delete。 CasecadeType.PERSIST 就像之前述的生命週期解說一樣, 把 parent和 child 一起拉到受管理的狀態。

註: CascadeType.REMOVE有點尷尬,似乎有更特別的使用規範。筆者測試過,在某些情況下,CascadeType.REMOVE無法處理ForeignKey問題,又或者是,刪除的順序不對。詳見 spring boot data deletion

Reference

馬交野