1. 引言

克隆 JPA 实体本质上是创建现有实体的副本。这让我们能在不影响原始对象的情况下修改新实体。本文将探讨几种克隆 JPA 实体的实用方法,帮你根据场景选择最合适的方案。

2. 为什么要克隆 JPA 实体?

有时我们需要在不修改原始实体的情况下复制数据,比如:

  • 创建与现有记录高度相似的新记录
  • 在内存中安全编辑实体,而不立即持久化到数据库

克隆通过复制实体让我们能在副本上操作,完美解决这些需求。

3. 克隆方案对比

克隆 JPA 实体有多种策略,每种对原始对象及其关联实体的复制深度不同。下面逐一分析:

3.1. 手动复制法

最简单粗暴的方式是手动复制字段。我们可以通过构造函数或显式设置每个字段值来实现,这能完全控制复制内容和关联关系处理。

先定义 ProductCategory 实体类:

@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() 方法中:

  1. 创建新的 Product 实例
  2. 显式复制原始对象的每个字段到新对象

⚠️ 踩坑提醒:JPA 中某些字段不应被复制:

  • ID 字段:JPA 通常自动生成 ID,复制会导致持久化冲突
  • 审计字段:如 createdBycreatedDate 等应重置,以反映新实体的生命周期

验证克隆行为的测试用例:

@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;
}

核心步骤:

  1. ByteArrayOutputStream 持有序列化数据
  2. writeObject()Product 转为字节序列
  3. 通过 ObjectInputStream 反序列化成新对象
  4. 显式重置 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());

核心逻辑:

  1. 分离原始实体后,JPA 不再跟踪其修改
  2. 修改 ID 和属性后重新持久化
  3. 原始记录保持不变

优点:直接利用 JPA 机制,无需额外代码
缺点:操作需谨慎,容易误改持久化上下文状态

4. 总结

本文对比了六种 JPA 实体克隆方案,适用场景总结如下:

方案 拷贝类型 适用场景 主要优势 主要缺点
手动复制 浅/深 简单实体,需精确控制字段 完全可控 复杂实体易遗漏字段
Cloneable 接口 需符合 Java 克隆规范的场景 标准实现 需手动处理关联对象
序列化 复杂嵌套结构 自动处理所有层级 性能差,需实现 Serializable
BeanUtils 快速复制简单实体 代码简洁 无法处理嵌套对象
ModelMapper 复杂嵌套实体 自动深拷贝 需额外依赖
JPA detach() - 需直接操作持久化上下文 无需额外代码 操作风险高

推荐选择

  • 简单实体 → 手动复制或 BeanUtils
  • 复杂嵌套实体 → ModelMapper
  • 需严格遵循 JPA 生命周期 → detach()
  • 性能不敏感且要深拷贝 → 序列化法

根据实际场景权衡利弊,避免踩坑!


原始标题:How to Clone a JPA Entity | Baeldung