科技新知
Spring Data 關聯型態 02
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