1. 引言
克隆 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 中某些字段不应被复制:
- ID 字段:JPA 通常自动生成 ID,复制会导致持久化冲突
- 审计字段:如
createdBy
、createdDate
等应重置,以反映新实体的生命周期
验证克隆行为的测试用例:
@Test
void whenUsingManualClone_thenReturnsNewEntityWithReferenceToRelatedEntities() {
// ...
Product clone = service.manualClone(original);
assertNotSame(original, clone);
assertSame(original.getCategory(), clone.getCategory());
}
测试显示 Category
仍是同一对象(浅拷贝)。若需要深拷贝 Category
,需额外克隆它:
Product manualDeepClone(Product original) {
Product clone = new Product();
clone.setName(original.getName());
// ... 其他字段
if (original.getCategory() != null) {
Category categoryClone = new Category();
categoryClone.setName(original.getCategory().getName());
// ... 其他字段
clone.setCategory(categoryClone);
}
return clone;
}
✅ 优点:精确控制复制字段和方式
❌ 缺点:实体变复杂时容易遗漏字段
3.2. 使用 Cloneable 接口
通过实现 Cloneable
接口并重写 clone()
方法实现克隆:
@Entity
public class Product implements Cloneable {
// ... 其他字段
}
重写 clone()
方法:
@Override
public Product clone() throws CloneNotSupportedException {
Product clone = (Product) super.clone();
clone.setId(null);
return clone;
}
调用 super.clone()
执行浅拷贝,需手动重置 ID。
3.3. 序列化法
通过序列化到字节流再反序列化实现克隆。此法适合深拷贝,能复制所有字段并处理复杂关系。
首先让实体实现 Serializable
:
@Entity
public class Product implements Serializable {
// ... 其他字段
}
序列化克隆实现:
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
转为字节序列- 通过
ObjectInputStream
反序列化成新对象 - 显式重置 ID 为
null
以符合 JPA 持久化要求
验证深拷贝的测试:
@Test
void whenUsingSerializationClone_thenReturnsNewEntityWithNewNestedEntities() {
// ...
Product clone = service.cloneUsingSerialization(original);
assertNotSame(original, clone);
assertNotSame(original.getCategory(), clone.getCategory());
}
✅ 优点:完美实现深拷贝(前提是所有嵌套对象可序列化)
❌ 缺点:
- 性能开销大(对象与字节流转换耗时)
- 需显式重置 ID
- 所有嵌套实体必须实现
Serializable
3.4. 使用 BeanUtils
利用 Spring 的 BeanUtils.copyProperties()
克隆实体。此法适合快速浅拷贝,无需手动设置每个属性。
添加依赖:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
克隆实现:
Product cloneUsingBeanUtils(Product original) throws InvocationTargetException, IllegalAccessException {
Product clone = new Product();
BeanUtils.copyProperties(original, clone);
clone.setId(null);
return clone;
}
⚠️ 注意:此法仅浅拷贝,不会复制嵌套的 category
字段。
验证测试:
@Test
void whenUsingBeanUtilsClone_thenReturnsNewEntityWithNullNestedEntities() throws InvocationTargetException, IllegalAccessException {
// ...
Product clone = service.cloneUsingBeanUtils(original);
assertNotSame(original, clone);
assertNull(clone.getCategory());
}
✅ 优点:快速实现浅拷贝
❌ 缺点:无法处理深拷贝,嵌套对象会被设为 null
3.5. 使用 ModelMapper
ModelMapper
能处理复杂对象的深拷贝,相比 BeanUtils
更擅长处理嵌套对象。
添加依赖:
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.2.1</version>
</dependency>
深拷贝实现:
Product cloneUsingModelMapper(Product original) {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setDeepCopyEnabled(true);
Product clone = modelMapper.map(original, Product.class);
clone.setId(null);
return clone;
}
关键配置:setDeepCopyEnabled(true)
启用深拷贝,递归复制嵌套字段。
验证深拷贝的测试:
@Test
void whenUsingModelMapperClone_thenReturnsNewEntityWithNewNestedEntities() {
// ...
Product clone = service.cloneUsingModelMapper(original);
assertNotSame(original, clone);
assertNotSame(original.getCategory(), clone.getCategory());
}
✅ 优点:自动处理复杂嵌套对象的深拷贝
❌ 缺点:需额外依赖
3.6. 使用 JPA 的 detach() 方法
JPA 的 detach()
方法可将实体从持久化上下文中分离。分离后修改再持久化,相当于创建新记录。
使用示例:
Product original = em.find(Product.class, 1L);
// 修改原始产品名称
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());
核心逻辑:
- 分离原始实体后,JPA 不再跟踪其修改
- 修改 ID 和属性后重新持久化
- 原始记录保持不变
✅ 优点:直接利用 JPA 机制,无需额外代码
❌ 缺点:操作需谨慎,容易误改持久化上下文状态
4. 总结
本文对比了六种 JPA 实体克隆方案,适用场景总结如下:
方案 | 拷贝类型 | 适用场景 | 主要优势 | 主要缺点 |
---|---|---|---|---|
手动复制 | 浅/深 | 简单实体,需精确控制字段 | 完全可控 | 复杂实体易遗漏字段 |
Cloneable 接口 | 浅 | 需符合 Java 克隆规范的场景 | 标准实现 | 需手动处理关联对象 |
序列化 | 深 | 复杂嵌套结构 | 自动处理所有层级 | 性能差,需实现 Serializable |
BeanUtils | 浅 | 快速复制简单实体 | 代码简洁 | 无法处理嵌套对象 |
ModelMapper | 深 | 复杂嵌套实体 | 自动深拷贝 | 需额外依赖 |
JPA detach() | - | 需直接操作持久化上下文 | 无需额外代码 | 操作风险高 |
推荐选择:
- 简单实体 → 手动复制或
BeanUtils
- 复杂嵌套实体 →
ModelMapper
- 需严格遵循 JPA 生命周期 →
detach()
- 性能不敏感且要深拷贝 → 序列化法
根据实际场景权衡利弊,避免踩坑!