如何克隆 JPA 實體
一、簡介
克隆 JPA 實體意味著建立現有實體的副本。這允許我們對新實體進行更改而不影響原始物件。在本教程中,我們將探索克隆 JPA 實體的各種方法。
2. 為什麼克隆JPA實體?
有時,我們希望複製資料而不修改原始實體。例如,我們可能想要建立一條與現有記錄幾乎相同的新記錄,或者我們可能需要安全地編輯記憶體中的實體而不立即將其儲存到資料庫中。
在這些情況下,克隆可以幫助我們複製實體並在副本上進行操作。
3. 克隆方法
複製 JPA 實體有多種策略,每種策略都會影響原始物件及其關聯實體的複製徹底程度。讓我們逐一探討。
3.1.使用手動複印
克隆實體最簡單的方法是手動複製其欄位。我們可以使用建構子或明確設定每個欄位值的方法。這使我們能夠完全控制複製的內容以及如何處理關係。
讓我們建立Product
實體和Category
實體類別:
@Entity
public class Category {
private Long id;
private String name;
// set and get
}
@Entity
public class Product {
private Long id;
private String name;
private double price;
private Category category;
// set and get
}
以下是我們使用方法手動複製Product
實體的欄位的範例:
Product manualClone(Product original) {
Product clone = new Product();
clone.setName(original.getName());
clone.setCategory(original.getCategory());
clone.setPrice(original.getPrice());
return clone;
}
在manualClone()
方法中,我們先建立Product
實體的新實例。然後,我們將每個欄位從原始產品明確複製到新的複製物件。
但是,在使用 JPA 時,在複製過程中不應複製某些欄位。例如,JPA 通常會自動產生 ID,因此複製 ID 欄位可能會導致在持久化複製實體時出現問題。
同樣,不應複製用於追蹤實體的建立和修改的審核字段,例如createdBy
、 createdDate
、 lastModifiedBy
和lastModifiedDate
。應重置這些欄位以反映新克隆的生命週期。
為了驗證克隆方法的行為,我們可以寫一個簡單的測試案例:
@Test
void whenUsingManualClone_thenReturnsNewEntityWithReferenceToRelatedEntities() {
// ...
Product clone = service.manualClone(original);
assertNotSame(original, clone);
assertSame(original.getCategory(), clone.getCategory());
}
在此測試中,我們看到Category
仍然引用相同的實體,表示淺複製。如果我們想要Category
的深層副本,我們還需要克隆它。
以下是克隆Product
時如何深度克隆Category
的範例:
Product manualDeepClone(Product original) {
Product clone = new Product();
clone.setName(original.getName());
// ... other fields
if (original.getCategory() != null) {
Category categoryClone = new Category();
categoryClone.setName(original.getCategory().getName());
// ... other fields
clone.setCategory(categoryClone);
}
return clone;
}
在這種情況下,我們建立一個新的Category
實例並手動複製其欄位以實現深度複製。
由於我們明確複製每個字段,因此我們可以準確地確定應複製哪些字段以及應如何複製它們。然而,隨著實體變得更加複雜,很容易忽略欄位。
3.2.使用Cloneable
介面
複製 JPA 實體的另一種方法是實作Cloneable
介面並重寫clone()
方法。
首先,我們需要確保我們的實體實作Cloneable
:
@Entity
public class Product implements Cloneable {
// ... other fields
}
然後我們重寫Product
實體中的clone()
方法:
@Override
public Product clone() throws CloneNotSupportedException {
Product clone = (Product) super.clone();
clone.setId(null);
return clone;
}
當我們呼叫super.clone()
時,該方法執行Product
物件的淺表複製。
3.3.使用序列化
克隆 JPA 實體的另一種方法是將其序列化為位元組流,然後將其反序列化回新物件。該技術非常適合深度克隆,因為它複製所有字段,甚至可以處理複雜的關係。
首先,我們需要確保我們的實體(如實作Serializable
介面):
@Entity
public class Product implements Serializable {
// ... other fields
}
一旦我們的實體可序列化,我們就可以使用序列化方法繼續複製它:
Product cloneUsingSerialization(Product original) throws IOException, ClassNotFoundException {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(original);
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream in = new ObjectInputStream(byteIn);
Product clone = (Product) in.readObject();
in.close();
clone.setId(null);
return clone;
}
在這個方法中,我們首先建立一個ByteArrayOutputStream
來保存序列化資料。 writeObject()
方法將Product
實例轉換為位元組序列並將其寫入ByteArrayOutputStream
。
接下來,使用ObjectInputStream
讀取位元組序列並將其反序列化回新的Product
物件。由於JPA實體需要唯一的標識符,因此我們明確地將ID設為null
,以確保當我們持久化複製的實體時。
為了驗證這個克隆方法,我們可以寫一個測試:
@Test
void whenUsingSerializationClone_thenReturnsNewEntityWithNewNestedEntities() {
// ...
Product clone = service.cloneUsingSerialization(original);
assertNotSame(original, clone);
assertNotSame(original.getCategory(), clone.getCategory());
}
在此測試中,複製的Product
是與original
物件不同的對象,包括任何巢狀實體,例如Category
。要注意的是,像Category
這樣的巢狀實體也必須實作Serializable
介面。
序列化對於深度克隆非常有用,因為它會複製所有字段,前提是所有嵌套物件和關係也是可序列化的。
但是,由於將物件與位元組流相互轉換的開銷,序列化可能會較慢。此外,我們必須明確地將ID
設為 null,以避免在持久化複製實體時發生衝突,因為 JPA 需要新持久化實體的新 ID。
3.4.使用BeanUtils
此外,我們也可以使用Spring框架的BeanUtils.copyProperties()
方法來複製實體。此方法將屬性值從一個物件複製到另一個物件。這種方法對於淺克隆很有用,我們需要快速複製實體的屬性,而無需手動設定每個屬性。
在使用這種方法之前,我們需要將[commons-beanutils](https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils/)
依賴項新增至 pom.xml 中:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
新增依賴項後,我們可以使用BeanUtils.copyProperties()
來實作 JPA 實體克隆:
Product cloneUsingBeanUtils(Product original) throws InvocationTargetException, IllegalAccessException {
Product clone = new Product();
BeanUtils.copyProperties(original, clone);
clone.setId(null);
return clone;
}
當我們想要快速、淺層複製實體時, BeanUtils
非常有用,儘管它不能很好地處理深層複製。因此category
欄位不會被複製。
讓我們用一個測試案例來驗證這個行為:
@Test
void whenUsingBeanUtilsClone_thenReturnsNewEntityWithNullNestedEntities() throws InvocationTargetException, IllegalAccessException {
// ...
Product clone = service.cloneUsingBeanUtils(original);
assertNotSame(original, clone);
assertNull(clone.getCategory());
}
在此範例中,它會建立一個新的Product
實例,但不會複製巢狀的Category
實體,並將Category
保留為null
。
同樣,使用此解決方案,我們僅執行淺複製。 ID 和審計相關屬性等欄位被有意排除或重置,因為此方法旨在解決 JPA 特定上下文中的快速實體重複問題。
3.5.使用ModelMapper
ModelMapper
是另一個實用程序,可以將一個物件對應到另一個物件。與執行淺複製的BeanUtils
不同, ModelMapper
旨在以最少的配置處理複雜的嵌套物件的深複製。在處理包含多個欄位或嵌套物件的大型實體時,它非常有效。
首先,將ModelMapper
依賴項加入我們的pom.xml
:
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.2.1</version>
</dependency>
以下是我們如何使用ModelMapper
克隆Product
實體:
Product cloneUsingModelMapper(Product original) {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setDeepCopyEnabled(true);
Product clone = modelMapper.map(original, Product.class);
clone.setId(null);
return clone;
}
在此範例中,我們使用modelMapper.map()
方法來複製Product
實體。此方法將原始Product
物件的欄位對應到新的Product
實例。
此外,它遞歸地遍歷物件及其嵌套字段,執行深度複製:
@Test
void whenUsingModelMapperClone_thenReturnsNewEntityWithNewNestedEntities() {
// ...
Product clone = service.cloneUsingModelMapper(original);
assertNotSame(original, clone);
assertNotSame(original.getCategory(), clone.getCategory());
}
在這個測試中,我們觀察到cloneUsingModelMapper()
方法建立了一個新的Product
實例,並為複製的產品建立了一個新的Category
實例。
3.6.使用 JPA 的detach()
方法
JPA 提供了detach()
方法來將實體與持久性上下文分開。分離實體後,我們可以對其進行修改並將其另存為新實體。當我們想要對實體進行最小的更改並將其視為資料庫中的新記錄時,此方法非常有用:
Product original = em.find(Product.class, 1L);
// Modify original product's name
original.setName("Smartphone");
em.merge(original);
original = em.find(Product.class, 1L);
em.detach(original);
original.setId(2L);
original.setName("Laptop");
Product clone = em.merge(original);
original = em.find(Product.class, 1L);
assertSame("Laptop", clone.getName());
assertSame("Smartphone", original.getName());
從測試案例中,我們將original
產品實例與持久性上下文分開。分離後,JPA 不會自動追蹤任何進一步的修改(例如更新名稱)。
4. 結論
在本文中,我們探討了克隆 JPA 實體的各種方法。手動複製是最直接的方法,可以完全控制要複製的欄位。
然而,對於更複雜的場景或處理關係時,自訂克隆方法或像ModelMapper
和BeanUtils
這樣的程式庫可能會很有用。
像往常一樣,這裡討論的程式碼可以在 GitHub 上找到。