如何在使用新生成的实体标识符时深度复制 Hibernate 实体

问题描述 投票:0回答:10

我正在使用关系数据库,使用单列 pk 和一些嵌套表。

我需要向我的项目添加一个简单的归档。归档仅在应用程序达到特定状态时发生,因此我希望做的是将现有的休眠对象复制到一个新实例中,其中新实例将使用新 ID 保存,同时保持现有对象完好无损。

我似乎无法弄清楚如何将现有对象复制到新实例中,而无需手动设置每个新实例字段。

有人知道一个简单的方法吗?

java hibernate jpa deep-copy identifier
10个回答
57
投票

只需检索对象,分离它,将 id 设置为 null 并保留它。

MyEntity clone = entityManager.find(MyEntity.class, ID);
entityManager.detach(clone);
clone.setId(null);
entityManager.persist(clone);

如果您的对象具有 oneToMany 关系,则必须对所有子对象重复该操作,但设置父对象 id(在

persist
调用后生成)而不是 null。

当然,您必须删除 OneToMany 关系上的任何

CASCADE persist
,否则您的持久操作将在 DB 或 fk 约束失败中创建所有子级的重复项。


15
投票

使用

detach
或其他人建议的深度克隆并不是克隆实体的正确方法。如果您尝试使此过程完全自动化,您将错过一点:并非所有属性都值得复制。

因此,您最好使用复制构造函数并准确控制需要克隆哪些属性。

所以,如果您有一个像这样的

Post
实体:

@Entity(name = "Post")
@Table(name = "post")
public class Post {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    @OneToMany(
        mappedBy = "post",
        cascade = CascadeType.ALL, 
        orphanRemoval = true
    )
    private List<PostComment> comments = new ArrayList<>();
 
    @OneToOne(
        mappedBy = "post",
        cascade = CascadeType.ALL, 
        orphanRemoval = true, 
        fetch = FetchType.LAZY
    )
    private PostDetails details;
 
    @ManyToMany
    @JoinTable(
        name = "post_tag",
        joinColumns = @JoinColumn(
            name = "post_id"
        ),
        inverseJoinColumns = @JoinColumn(
            name = "tag_id"
        )
    )
    private Set<Tag> tags = new HashSet<>();
 
    //Getters and setters omitted for brevity
 
    public void addComment(
            PostComment comment) {
        comments.add(comment);
        comment.setPost(this);
    }
 
    public void addDetails(
            PostDetails details) {
        this.details = details;
        details.setPost(this);
    }
 
    public void removeDetails() {
        this.details.setPost(null);
        this.details = null;
    }
}

复制

comments
并将其用作新模板时,克隆
Post
是没有意义的:

Post post = entityManager.createQuery("""
    select p
    from Post p
    join fetch p.details
    join fetch p.tags
    where p.title = :title
    """, Post.class)
.setParameter(
    "title", 
    "High-Performance Java Persistence, 1st edition"
)
.getSingleResult();
 
Post postClone = new Post(post);
postClone.setTitle(
    postClone.getTitle().replace("1st", "2nd")
);
entityManager.persist(postClone);

您需要向

Post
实体添加一个复制构造函数:

/**
 * Needed by Hibernate when hydrating the entity 
 * from the JDBC ResultSet
 */
private Post() {}
 
public Post(Post post) {
    this.title = post.title;
 
    addDetails(
        new PostDetails(post.details)
    );
 
    tags.addAll(post.getTags());
}

因此,复制构造函数是解决实体克隆/复制问题的最佳方法。


11
投票

我也在使用 Hibernate,我得到了与你相同的要求。我遵循的是实施

Cloneable
。下面是如何执行此操作的代码示例。

class Person implements Cloneable {

        private String firstName;
        private String lastName;

        public Object clone() {

            Person obj = new Person();
            obj.setFirstName(this.firstName);
            obj.setLastName(this.lastName);

            return obj;
        }

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
    }

或者您可以使用基于反射的解决方案,但我不会推荐这样做。检查这个网站了解更多详情。


3
投票

看看下面的链接。最强大的克隆机制之一,可以在休眠中以最有效的方式使用

https://thoughtfulsoftware.blogspot.com/2013/05/using-variable-depth-copy-to-prevent.html


2
投票

您可以克隆该对象并擦除 ID,然后将其保存回来,以便分配新的 ID。

所以

Person person = EmployeeDAO.get(empId);
Person newPersonCopy = person.createCopyWithNoId()
EmployeeDAO.add(newPersonCopy);

在这种情况下,createCopyWithNoId()将创建对象的克隆并将 Id 字段设置为 null。当您现在添加新的克隆时,休眠引擎会将其视为新对象并将其保留,并且数据库将分配一个新的主键。

请注意,我避免调用克隆方法,因为当我们操作 Id(将其设置为空)时,结果并不是精确的克隆;

另一种方法是添加一个构造函数,该构造函数接受 person 类型的对象并创建一个新对象,但只是不设置 Id 字段,使其保留默认值 null。你再次坚持使用 DAO。


1
投票

如果对象是可克隆的,您可以克隆,或者您可以为要复制的对象定义一个方法/构造函数,获取其自身的参数并将您需要的所有内容复制到一个新实例中并将其返回给您。


1
投票

1- 将 commons-lang 依赖项添加到 pom.xml 文件

<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>

2- 导入

org.apache.commons.lang.SerializationUtils


3-使用
SerializationUtils.clone(oldObj)


前任。
Class1 newObj = (Class1) SerializationUtils.clone(oldObj);

另请参阅java-deep-copy


1
投票

我们有类似的需求,我们需要在交易的某个点复制实体并将其用于审计。解决方案非常简单。我们可以使用 Json 对象映射器。

public class Employee {
   public int id;
   public String name;
}

Employee employee = entityManager.find(Employee.class, ID); 

ObjectMapper mapper = new ObjectMapper();
Employee deepCopiedEmployee = mapper.readValue( mapper.writeValueAsString( employee ), Employee.class );
deepCopiedEmployee.setId(null);

另一种选择是使用内部 Spring Utils。

org.springframework.beans.BeanUtils.copyProperties(<source>, <target>, ...ignoredPropertied)

Employee copiedEmployee = new Employee();
BeanUtils.copyProperties(employee, copiedEmployee, id)

注意:使用BeanUtils时,Bean将在事务中被跟踪。即,如果在事务期间对源 bean 所做的任何更改都将反映在目标复制的 bean 中。


0
投票

您可以使用mapstruct并创建从一个对象到另一个对象的方法复制,同时忽略 id 和您不想复制的每个字段。

如果您有一对多、多对多等关联,您还必须在其映射器中具有复制方法。

为了约束孩子与父母, 您可以使用 afterMapping 注释并添加类似 Vlad Mihalcea 发布的方法,并在那里完成您的工作,或者只是在服务中完成它


0
投票

另一种方法是使用 mapstruct 生成复制方法。它使用 getter/setter 并在编译时生成代码。

示例:

@Mapper
public interface SettingsMapper {
    SettingsMapper INSTANCE = Mappers.getMapper(SettingsMapper.class);

    @Mapping(target = "id", ignore = true)
    Settings copy(Settings settings);
}

Settings copy = SettingsMapper.INSTANCE.copy(defaultSettings);
© www.soinside.com 2019 - 2024. All rights reserved.