Java 持久化/持久化
JPA 使用 EntityManager API 進行執行時使用。EntityManager 代表應用程式會話或與資料庫的對話。每個請求或每個客戶端都將使用自己的 EntityManager 來訪問資料庫。EntityManager 還代表一個事務上下文,在典型的無狀態模型中,每個事務都會建立一個新的 EntityManager。在有狀態模型中,EntityManager 可能與客戶端會話的生命週期相匹配。
EntityManager 提供了所有必需的持久化操作的 API。這些包括以下 CRUD 操作
EntityManager 是一個面向物件的 API,因此不會直接對映到資料庫 SQL 或 DML 操作。例如,要更新一個物件,您只需要讀取該物件並透過其 set 方法更改其狀態,然後在事務上呼叫 commit。EntityManager 會找出您更改了哪些物件並對資料庫執行正確的更新,JPA 中沒有顯式的更新操作。
JPA 為給定持久化上下文的某個物件定義了兩種主要狀態,即 託管的 和 分離的。
託管物件是在當前持久化上下文(EntityManager/JTA 事務)中讀取的物件。託管物件在持久化上下文中註冊,持久化上下文將跟蹤對該物件的更改並維護其物件標識。如果在同一個持久化上下文中再次讀取同一個物件,或者透過另一個託管物件的關聯關係進行遍歷,則將返回同一個相同(==)的物件。在新的物件上呼叫 persist 也將使其成為託管物件。在分離物件上呼叫 merge 將返回該物件的託管副本。一個物件永遠不應該由多個持久化上下文進行託管。一個物件將由其持久化上下文進行託管,直到持久化上下文透過 clear 被清除,或者該物件透過 detach 被強制分離。刪除的物件在 flush 或 commit 之後將不再被託管。在 rollback 上,所有託管物件將變為分離物件。在 JTA 託管的 EntityManager 中,所有託管物件在任何 JTA commit 或 rollback 上都將變為分離物件。
分離物件是在當前持久化上下文不被託管的物件。這可能是透過不同的持久化上下文讀取的物件,或者是被克隆或序列化後的物件。新物件在對它呼叫 persist 之前也被認為是分離的。被刪除並重新整理或提交的物件將變為分離物件。一個物件可以在一個持久化上下文中被認為是託管的,在另一個持久化上下文中被認為是分離的。
託管物件應該只引用其他託管物件,而分離物件應該只引用其他分離物件。避免關聯或混合分離的和託管的物件,這通常會導致問題,因為您的應用程式可能會訪問同一物件的兩個副本,從而導致更改丟失或資料陳舊。錯誤地關聯託管的和分離的物件可能是使用者在 JPA 中遇到的最常見問題之一。
EntityManager.persist() 操作用於將新物件插入資料庫。persist 不會直接將物件插入資料庫:它只是在持久化上下文(事務)中將其註冊為新的。當事務被提交,或者持久化上下文被重新整理時,該物件將被插入資料庫。
如果該物件使用生成的 Id,則 Id 通常會在呼叫 persist 時被分配給該物件,因此 persist 也可以用於分配物件的 Id。唯一的例外是如果使用 IDENTITY 順序,在這種情況下,Id 僅在 commit 或 flush 時分配,因為資料庫僅在 INSERT 時分配 Id。如果該物件不使用生成的 Id,則通常應該在呼叫 persist 之前分配其 Id。
persist 操作只能在事務內呼叫,在事務之外呼叫會丟擲異常。persist 操作是就地進行的,也就是說,要持久化的物件將成為持久化上下文的一部分。事務提交時物件的 state 將被持久化,而不是 persist 呼叫時的 state。
persist 通常只應該在新物件上呼叫。如果物件是持久化上下文的一部分,則允許在現有物件上呼叫它,這僅用於將級聯持久化操作應用於任何可能的相關新物件。如果在不是持久化上下文一部分的現有物件上呼叫 persist,則可能會丟擲異常,或者可能會嘗試插入併發生資料庫約束錯誤,或者如果未定義約束,則可能能夠插入重複資料。
persist 只能在 Entity 物件上呼叫,不能在 Embeddable 物件、集合或非持久化物件上呼叫。Embeddable 物件將自動作為其所屬 Entity 的一部分進行持久化。
並非總是需要呼叫 persist。如果您將一個新物件關聯到持久化上下文的一部分的現有物件,並且關聯關係是級聯持久化的,那麼當事務被提交或持久化上下文被重新整理時,它將被自動插入。
EntityManager em = getEntityManager();
em.getTransaction().begin();
Employee employee = new Employee();
employee.setFirstName("Bob");
Address address = new Address();
address.setCity("Ottawa");
employee.setAddress(address);
em.persist(employee);
em.getTransaction().commit();
在物件上呼叫 persist 還將在所有標記為級聯持久化的關聯關係上級聯 persist 操作。如果關聯關係不是級聯持久化的,並且關聯物件是新的,那麼如果您沒有首先在關聯物件上呼叫 persist,則可能會丟擲異常。從直覺上說,您可能會考慮將所有關聯關係標記為級聯持久化,以避免必須在每個物件上呼叫 persist,但這也會導致問題。
將所有關係標記為級聯持久化會導致效能問題。在每次持久化呼叫時,都需要遍歷所有相關物件並檢查它們是否引用了任何新物件。如果將所有關係標記為級聯持久化,並持久化一個大型的新物件圖,這實際上會導致 `O(n²) ` 效能問題。如果僅對根物件呼叫 `persist`,則可以。但是,如果對圖中的每個物件呼叫 `persist`,則會遍歷圖中的每個物件,這會導致嚴重的效能問題。JPA 規範可能應該將 `persist` 定義為僅適用於新物件,而不是已存在的持久化上下文的一部分,但它要求 `persist` 應用於所有物件,無論是新的、現有的還是已持久化的,因此會出現此問題。
第二個問題是,如果呼叫 `remove` 刪除一個物件,然後呼叫該物件的 `persist`,它將恢復該物件,並且它將再次變為持久化狀態。如果這是故意的,這可能是需要的,但 JPA 規範也要求級聯持久化具有此行為。因此,如果呼叫 `remove` 刪除了一個物件,但忘記從級聯持久化關係中刪除對它的引用,則 `remove` 將被忽略。
建議只將複合關係或私有關係標記為級聯持久化。
合併
[edit | edit source]EntityManager.merge() 操作用於將對分離物件的更改合併到持久化上下文。merge 不會直接將物件更新到資料庫,而是將更改合併到持久化上下文(事務)中。當事務提交或持久化上下文被 *重新整理* 時,物件將在資料庫中被更新。
通常不需要 merge,儘管它經常被誤用。要更新一個物件,只需讀取它,然後透過它的 `set` 方法更改它的狀態,然後提交事務。EntityManager 將找出所有更改並更新資料庫。只有在具有持久化物件的離線副本時,才需要 merge。*離線* 物件是指透過不同的 EntityManager(或在 JEE 管理的 EntityManager 中的不同事務)讀取的物件,或已克隆或序列化的物件。一個常見的情況是 stateless `SessionBean`,其中物件在一個事務中讀取,然後在另一個事務中更新。由於更新是在不同的事務中處理的,並且具有不同的 EntityManager,因此必須先進行合併。merge 操作將查詢/找到離線物件的託管物件,並將離線物件中發生更改的每個屬性複製到託管物件中,以及級聯任何標記為級聯合並的相關物件。
merge 操作只能在事務中呼叫,在事務之外呼叫會丟擲異常。merge 操作不是原位操作,也就是說,要合併的物件永遠不會成為持久化上下文的一部分。任何進一步的更改都必須對 merge 返回的託管物件進行,而不是對離線物件進行。
merge 通常用於現有的物件,但也可以用於新的物件。如果物件是新的,將建立該物件的副本並將其註冊到持久化上下文,離線物件本身不會被持久化。
merge 只能用於 `Entity` 物件,不能用於 `Embeddable` 物件、集合或非持久化物件。Embeddable 物件作為其擁有 `Entity` 的一部分自動合併。
合併示例
[edit | edit source]EntityManager em = createEntityManager();
Employee detached = em.find(Employee.class, id);
em.close();
...
em = createEntityManager();
em.getTransaction().begin();
Employee managed = em.merge(detached);
em.getTransaction().commit();
級聯合並
[edit | edit source]對物件呼叫 merge 也會級聯 merge 操作到任何標記為級聯合並的關係中。即使關係不是級聯合並,引用也會被合併。如果關係是級聯合並,關係和每個相關物件都將被合併。直覺上,你可能會考慮將每個關係標記為級聯合並,以避免不得不擔心對每個物件呼叫合併,但這通常不是一個好主意。
將所有關係標記為級聯合並會導致效能問題。如果一個物件具有很多關係,那麼每次 merge 呼叫都需要遍歷一個大型的物件圖。
另一個問題是,如果離線物件在某種程度上已損壞。例如,假設有一個 Employee,他有一個 manager,但該 manager 有一個不同的離線 Employee 物件副本作為其 managedEmployee。這會導致同一個物件被合併兩次,或者至少可能無法一致地確定哪個物件將被合併,因此你可能無法獲得預期的合併更改。如果你沒有更改物件,但其他使用者更改了,如果 merge 級聯到此未更改的物件,它將還原其他使用者的更改,或者丟擲 OptimisticLockException(取決於你的鎖定策略)。這通常不可取。
建議只將複合關係或私有關係標記為級聯合並。
瞬態變數
[edit | edit source]merge 的另一個問題是瞬態變數。由於 merge 通常與物件序列化一起使用,如果關係被標記為 transient(Java 瞬態,而不是 JPA 瞬態),那麼離線物件將包含 null,並且 null 將被合併到物件中,即使這是不希望的。即使關係不是級聯合並,也會發生這種情況,因為 merge 始終合併對相關物件的引用。通常,在使用序列化時,需要瞬態,以避免在只要求單個物件或一小部分物件的情況下序列化整個資料庫。
一種解決方案是避免將任何內容標記為 transient,而是使用 JPA 中的 `LAZY` 關係來限制要序列化的內容(未訪問的延遲關係通常不會被序列化)。另一種解決方案是在自己的程式碼中手動合併。
一些 JPA 提供程式提供擴充套件的 merge 操作,例如允許 *淺* 合併或 *深* 合併,或不合並引用的合併。
刪除
[edit | edit source]EntityManager.remove() 操作用於從資料庫中刪除物件。remove 不會直接從資料庫中刪除物件,而是標記物件在持久化上下文(事務)中被刪除。當事務提交或持久化上下文被 *重新整理* 時,物件將從資料庫中被刪除。
remove 操作只能在事務中呼叫,在事務之外呼叫會丟擲異常。remove 操作必須在託管物件上呼叫,而不是在離線物件上呼叫。通常,必須先 `find` 物件才能刪除它,儘管可以對物件的 `Id` 呼叫 EntityManager.getReference(),然後對引用呼叫刪除。根據 JPA 提供程式如何最佳化 getReference 和 remove,它可能不需要從資料庫中讀取物件。
remove 只能用於 `Entity` 物件,不能用於 `Embeddable` 物件、集合或非持久化物件。Embeddable 物件作為其擁有 `Entity` 的一部分自動刪除。
刪除示例
[edit | edit source]EntityManager em = getEntityManager();
em.getTransaction().begin();
Employee employee = em.find(Employee.class, id);
em.remove(employee);
em.getTransaction().commit();
級聯刪除
[edit | edit source]對物件呼叫 remove 也會級聯 remove 操作到任何標記為級聯刪除的關係中。
注意,級聯刪除隻影響 remove 呼叫。如果有一個級聯刪除的關係,並且從集合中刪除了一個物件,或取消引用了一個物件,它 *不會* 被刪除。必須明確呼叫 remove 才能刪除物件。一些 JPA 提供程式提供擴充套件來提供此行為,在 JPA 2.0 中,`OneToMany` 和 `OneToOne` 對映將有一個 `orphanRemoval` 選項來提供此行為。
轉世
[edit | edit source]通常情況下,被移除的物件將保持移除狀態,但在某些情況下,您可能需要將物件恢復。這種情況通常發生在使用自然 ID(而非生成的 ID)時,因為新物件始終會獲得一個新的 ID。通常,恢復物件的願望源於糟糕的物件模型設計,通常是想要更改物件的類型別(這在 Java 中無法做到,因此必須建立新的物件)。通常情況下,最佳解決方案是更改物件模型,使物件包含一個型別物件來定義其型別,而不是使用繼承。但有時恢復物件也是可取的。
如果在兩個單獨的事務中執行此操作,通常情況下是沒問題的,首先您remove物件,然後您persist它。如果您想在同一事務中remove和persist具有相同Id的物件,則操作會更加複雜。如果您對某個物件呼叫remove,然後對同一物件呼叫persist,那麼它將不再被移除。如果您對某個物件呼叫remove,然後對具有相同Id的另一個物件呼叫persist,那麼行為可能取決於您的 JPA 提供程式,而且可能無法正常工作。如果您在呼叫remove後呼叫flush,然後呼叫persist,那麼該物件應該可以成功恢復。請注意,它將是一行新的資料,現有資料行將被刪除,並且將插入一行新的資料。如果您希望更新同一行資料,則可能需要使用本機 SQL 更新查詢。
高階
[edit | edit source]重新整理
[edit | edit source]EntityManager.refresh() 操作用於從資料庫中重新整理物件的狀態。這將恢復當前事務中對物件進行的任何未重新整理的更改,並將物件的狀態重新整理為當前在資料庫中定義的狀態。如果發生了flush,它將重新整理為已重新整理的狀態。重新整理必須在受管物件上呼叫,因此如果您有非受管例項,則可能需要使用活動的EntityManager先find該物件。
重新整理將級聯到任何標記為cascade重新整理的關係,儘管它可能會根據您的獲取型別以延遲方式進行,因此您可能需要訪問該關係以觸發重新整理。refresh只能在Entity物件上呼叫,不能在Embeddable物件、集合或非持久物件上呼叫。Embeddable物件會作為其擁有Entity的一部分自動重新整理。
重新整理可用於恢復更改,或者如果您的 JPA 提供程式支援快取,則可用於重新整理過時的快取資料。有時,希望Query或find操作重新整理結果。不幸的是,JPA 1.0 沒有定義如何執行此操作。一些 JPA 提供程式提供了查詢提示,允許在查詢上啟用重新整理。
- TopLink / EclipseLink : 定義查詢提示
"eclipselink.refresh"以允許在查詢上啟用重新整理。
JPA 2.0 定義了一組用於重新整理的標準查詢提示,請參閱JPA 2.0 快取 API。
重新整理示例
[edit | edit source]EntityManager em = getEntityManager();
em.refresh(employee);
鎖定
[edit | edit source]請參閱讀寫鎖定。
獲取引用
[edit | edit source]EntityManager.getReference() 操作用於獲取對物件的控制代碼,而無需載入該物件。它類似於find操作,但可能返回代理或未提取物件。JPA 不要求getReference避免載入物件,因此某些 JPA 提供程式可能不支援它,而只是執行正常的查詢操作。getReference返回的物件應該看起來像一個普通物件,如果您訪問除Id以外的任何方法或屬性,它將觸發從資料庫中重新整理自身。
getReference的目的是,如果只有物件的Id並且想要避免載入物件,則可以在插入或更新操作中使用它作為相關物件的替代物件。請注意,getReference不會像find那樣驗證物件的是否存在。如果物件不存在,並且您嘗試在插入或更新中使用未提取物件,則可能會發生外部索引鍵約束衝突,或者如果您訪問該物件,則可能會觸發異常。
獲取引用示例
[edit | edit source]EntityManager em = getEntityManager();
Employee manager = em.getReference(Employee.class, managerId);
Employee employee = new Employee();
...
em.persist(employee);
employee.setManager(manager);
em.commit();
重新整理
[edit | edit source]EntityManager.flush() 操作可用於在事務提交之前將所有更改寫入資料庫。預設情況下,JPA 通常不會在事務提交之前將更改寫入資料庫。這通常是可取的,因為它可以避免在需要之前訪問資料庫、資源和鎖。它還允許資料庫寫入按順序進行,並以最佳方式進行批處理以訪問資料庫,並維護完整性約束,避免死鎖。這意味著,當您呼叫persist、merge或remove時,資料庫 DML INSERT, UPDATE, DELETE不會執行,直到提交或觸發重新整理。
flush()不會執行實際的commit:commit仍然會在請求顯式commit()時發生(在資源本地事務的情況下),或者在容器管理(JTA)事務完成時發生。
重新整理有多種用途
- 在執行查詢之前重新整理更改,以使查詢能夠返回新物件和持久化單元中進行的更改。
- 插入持久物件,以確保其
Id被分配並可供應用程式訪問(如果使用IDENTITY排序)。 - 將所有更改寫入資料庫,以便對任何資料庫錯誤進行錯誤處理(在使用 JTA 或 SessionBean 時非常有用)。
- 重新整理和清除批處理,以便在單個事務中進行批處理。
- 避免約束錯誤或恢復物件。
重新整理示例
[edit | edit source]public long createOrder(Order order) throws ACMEException {
EntityManager em = getEntityManager();
em.persist(order);
try {
em.flush();
} catch (PersistenceException exception) {
throw new ACMEException(exception);
}
return order.getId();
}
清除
[edit | edit source]EntityManager.clear() 操作可用於清除持久化上下文。這將清除當前EntityManager或事務中讀取、更改、持久化或移除的所有物件。已經透過flush寫入資料庫的更改,或對資料庫進行的任何更改都不會被清除。透過EntityManager讀取或持久化的任何物件都將被分離,這意味著對該物件進行的任何更改都不會被跟蹤,並且在合併到新的持久化上下文中之前,不應再使用該物件。
clear可用於類似於回滾的操作,以放棄更改並重新啟動持久化上下文。如果事務提交失敗或執行回滾,則持久化上下文將自動被清除。
clear類似於關閉EntityManager並建立一個新的EntityManager,主要區別在於clear可以在事務正在進行時呼叫。clear還可用於釋放EntityManager消耗的物件和記憶體。重要的是要注意,EntityManager負責跟蹤和管理在其持久化上下文中讀取的所有物件。在應用程式管理的EntityManager中,這包括自建立EntityManager以來讀取的每個物件,包括EntityManager使用的每個事務。如果使用長期存在的EntityManager,這將是一個內在的記憶體洩漏,因此呼叫clear或關閉EntityManager並建立一個新的EntityManager是重要的應用程式設計考量。對於 JTA 管理的EntityManager,持久化上下文會在每個 JTA 事務邊界自動被清除。
清除對於大型批處理作業也很重要,即使它們在一個事務中發生。批處理作業可以在同一事務中拆分為更小的批次,並且可以在每個批次之間呼叫clear以避免持久化上下文變得過大。
public void processAllOpenOrders() {
EntityManager em = getEntityManager();
List<Long> openOrderIds = em.createQuery("SELECT o.id from Order o where o.isOpen = true");
em.getTransaction().begin();
try {
for (int batch = 0; batch < openOrderIds.size(); batch += 100) {
for (int index = 0; index < 100 && (batch + index) < openOrderIds.size(); index++) {
Long id = openOrderIds.get(batch + index);
Order order = em.find(Order.class, id);
order.process(em);
}
em.flush();
em.clear();
}
em.getTransaction().commit();
} catch (RuntimeException error) {
if (em.getTransaction().isActive()) {
em.getTransaction().rollback();
}
}
}
該 EntityManager.close() 操作用於釋放應用程式管理的 EntityManager 的資源。JEE JTA 管理的 EntityManager 無法關閉,因為它們由 JTA 事務和 JEE 伺服器管理。
EntityManager 的生命週期可以持續一個事務、請求或使用者會話。通常,生命週期為每個請求,並且 EntityManager 在請求結束時關閉。從 EntityManager 獲取的物件在 EntityManager 關閉時變為分離狀態,並且如果在 EntityManager 關閉之前未訪問過任何 LAZY 關係,則可能無法再訪問它們。一些 JPA 提供商允許在關閉後訪問 LAZY 關係。
public Order findOrder(long id) {
EntityManager em = factory.createEntityManager();
Order order = em.find(Order.class, id);
order.getOrderLines().size();
em.close();
return order;
}
該 EntityManager.getDelegate() 操作用於訪問 JPA 提供商的 EntityManager 實現類,該類位於 JEE 管理的 EntityManager 中。JEE 管理的 EntityManager 將由 JEE 伺服器中的一個代理 EntityManager 包裹,該伺服器將請求轉發到當前 JTA 事務中活動的 EntityManager。如果需要 JPA 提供商特定的 API,則 getDelegate() API 允許訪問 JPA 實現以呼叫該 API。
在 JEE 中,管理的 EntityManager 通常會為每個 JTA 事務建立一個新的 EntityManager。此外,在 JTA 事務上下文之外的行為是有些不明確的。在 JTA 事務上下文之外,JEE 管理的 EntityManager 可能會為每個方法建立一個新的 EntityManager,因此 getDelegate() 可能會返回一個臨時的 EntityManager 甚至 null。訪問 JPA 實現的另一種方法是透過 EntityManagerFactory,該工廠通常不會被代理包裹,但可能在某些伺服器中被包裹。
在 JPA 2.0 中,getDelegate() API 已被更通用的 unwrap() API 替換。
public void clearCache() {
EntityManager em = getEntityManager();
((JpaEntityManager)em.getDelegate()).getServerSession().getIdentityMapAccessor().initializeAllIdentityMaps();
}
該 EntityManager.unwrap() 操作用於訪問 JPA 提供商的 EntityManager 實現類,該類位於 JEE 管理的 EntityManager 中。JEE 管理的 EntityManager 將由 JEE 伺服器中的一個代理 EntityManager 包裹,該伺服器將請求轉發到當前 JTA 事務中活動的 EntityManager。如果需要 JPA 提供商特定的 API,則 unwrap() API 允許訪問 JPA 實現以呼叫該 API。
在 JEE 中,管理的 EntityManager 通常會為每個 JTA 事務建立一個新的 EntityManager。此外,在 JTA 事務上下文之外的行為是有些不明確的。在 JTA 事務上下文之外,JEE 管理的 EntityManager 可能會為每個方法建立一個新的 EntityManager,因此 getDelegate() 可能會返回一個臨時的 EntityManager 甚至 null。訪問 JPA 實現的另一種方法是透過 EntityManagerFactory,該工廠通常不會被代理包裹,但可能在某些伺服器中被包裹。
public void clearCache() {
EntityManager em = getEntityManager();
em.unwrap(JpaEntityManager.class).getServerSession().getIdentityMapAccessor().initializeAllIdentityMaps();
}