【问题标题】:How to clone a JPA entity如何克隆 JPA 实体
【发布时间】:2012-07-22 10:19:57
【问题描述】:

我有一个 JPA 实体已经保存在数据库中。
我想要一份它的副本(带有不同的 id),修改了一些字段。

最简单的方法是什么?喜欢:

  • 将其 @Id 字段设置为 null 并持久化它会起作用吗?
  • 我是否必须为实体创建一个克隆方法(复制除@Id 之外的所有字段)?
  • 还有其他方法(例如使用克隆框架)吗?

【问题讨论】:

  • 嗯,“因为过于宽泛而封闭”——为什么?这个问题很好,而且非常具体地说明了需要什么。
  • 您有一个复制构造函数,您可以在其中复制所需的字段值。这与 JPA API 完全无关。基础 Java。

标签: java jpa orm entity clone


【解决方案1】:

使用EntityManager.detach。它使 bean 不再链接到 EntityManager。然后将 Id 设置为新 Id(如果自动为 null),更改您需要的字段并保留。

【讨论】:

  • 我正在使用现有实体执行此操作,即我基本上是在更改实体对象的主键 (ID) 并将其合并,以便使用其他值更新所需的对象。这有什么风险吗?到目前为止,它似乎工作正常。
  • 我们在使用这种技术时遇到的一个问题是detach 将忽略对托管实体的未刷新更改。例如如果您想 (1) 修改托管实体,(2) 分离实体,以及 (3) 保留副本,则必须在分离之前调用 flush,否则您的修改将不会保留。来自flush 的 Javadoc:“对实体所做的未刷新更改(如果有)(包括删除实体)将不会同步到数据库。”
  • 对于那些使用 Spring Data JPA(及其自动生成的存储库实现)的用户,请参阅:stackoverflow.com/a/26812963/56285
  • 如果entity有表关系和@OneToMany(cascade = CascadeType.ALL, mappedBy = "FIELD_NAME")这样的级联,你可能需要在persist之前循环每个引用实体对象并重置Id,否则可能会抛出PersistentObjectException: detached entity passed to persist
  • @javaMS 两种风险浮现在脑海中:当被分离的实体在会话中其他实体的集合中被引用时会发生什么;这在使用延迟加载/代理时是否正常工作。没有深入了解它。
【解决方案2】:

在使用 EclipseLink 时,您可以使用非常方便的 CopyGroup-Feature:

http://wiki.eclipse.org/EclipseLink/Examples/JPA/AttributeGroup#CopyGroup

一个很大的优点是,它也无需太多改动就可以正确克隆私有关系。

这是我的代码,克隆播放列表及其私有的@OneToMany-relationship 只需几行代码:

public Playlist cloneEntity( EntityManager em ) {
    CopyGroup group = new CopyGroup();
    group.setShouldResetPrimaryKey( true );
    Playlist copy = (Playlist)em.unwrap( JpaEntityManager.class ).copy( this, group );
    return copy;
}

确保你使用persist()来保存这个新对象,merge()不起作用。

【讨论】:

    【解决方案3】:

    您最好使用复制构造函数并准确控制需要克隆的属性。

    所以,如果你有一个像这样的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;
        }
    }
    

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

    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 实体的是copy constructor

    /**
     * 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());
    }
    

    这是解决实体克隆/复制问题的最佳方法。任何其他试图使该过程完全自动化的方法都忽略了并非所有属性都值得复制这一点。

    【讨论】:

    • 您发布的代码暗示您正在编写您的书的第二版……(太好了!我有第一版)
    • 我一定会出版第二版。我正在等待 Hibernate 6 发布,并将在 2020 年发布。
    • 如果您必须复制 100 多个实体(每个实体具有多个关系),这仍然是一个高性能选项吗?
    • 回答这个问题的唯一方法是编写一个 JMH 测试来衡量性能。
    【解决方案4】:

    我今天面临同样的问题:我在数据库中有一个实体,我想:

    • 从数据库中获取
    • 更改其属性值之一
    • 创建它的克隆
    • 只修改克隆的一些属性
    • 在数据库中持久化克隆

    我成功完成了以下步骤:

    @PersistenceContext(unitName = "...")
    private EntityManager entityManager;
    
    public void findUpdateCloneAndModify(int myEntityId) {
      // retrieve entity from database
      MyEntity myEntity = entityManager.find(MyEntity.class, myEntityId);
      // modify the entity
      myEntity.setAnAttribute(newValue);
      // update modification in database
      myEntity = entityManager.merge(myEntity);
      // detach entity to use it as a new entity (clone)
      entityManager.detach(myEntity);
      myEntity.setId(0);
      // modify detached entity
      myEntity.setAnotherAttribute(otherValue);
      // persist modified clone in database
      myEntity = entityManager.merge(myEntity);
    }
    

    备注:如果我使用“persist”而不是“merge”,最后一步(克隆持久性)不起作用,即使我在调试模式下注意到克隆 id 在“persist”之后已更改命令!

    我仍然面临的问题是我的第一个实体在分离之前没有被修改。

    【讨论】:

    • 使用 EclipseLink 2.7.4 merge 抛出异常,提示无法更新主键。 persist 工作正常。
    【解决方案5】:

    您可以使用像 Orika 这样的映射框架。 http://orika-mapper.github.io/orika-docs/ Orika 是一个 Java bean 映射框架,它递归地将数据从一个对象复制到另一个对象。它易于配置并提供各种灵活性。

    这是我在项目中使用它的方式: 添加了一个依赖:

     <dependency>
          <groupId>ma.glasnost.orika</groupId>
          <artifactId>orika-core</artifactId>
          <version>1.4.6</version>
    </dependency>
    

    然后在代码中使用如下:

    MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
    MapperFacade mapper=mapperFactory.getMapperFacade();
    User mappedUser = mapper.map(oldUser, User.class);
    

    如果您有许多需要此类克隆的用例,这可能会有所帮助。

    【讨论】:

      【解决方案6】:

      正如已接受答案的 cmets 中所述,detatch 将忽略对托管实体的未刷新更改。 如果您使用的是 spring,您还有另一个选择是使用 org.springframework.beans.BeanUtils

      这里有BeanUtils.copyProperties(Object source, Object target)。这将允许您在不篡改 entityManager 的情况下进行浅拷贝。

      编辑: 来自 api doc 的引用:“此方法旨在执行属性的“浅拷贝”,因此不会复制复杂的属性(例如,嵌套的)。”

      This 博文可能会告诉您更多关于深度复制 java 对象的信息。

      【讨论】:

      • 这种方法似乎最适合我的目的。我只是使用BeanUtils.copyProperties(source, target, "id") 并制作一个没有其 ID 属性的副本,然后将其持久化,并为其分配一个 ID。
      • 当存在一对多/多对一关系时这会起作用吗?
      • @nurettin,没有。这是一个浅拷贝。来自 api doc 的引用:“此方法旨在执行属性的“浅拷贝”,因此不会复制复杂的属性(例如,嵌套的)。”试试这个深拷贝:javaworld.com/article/2077578/learn-java/…
      【解决方案7】:

      我刚刚尝试将 id 设置为 null 并且它起作用了

      address.setId(null);
      address = addrRepo.save(address);
      

      将 id 设置为 null 使其保存到具有新 id 的新记录中,因为我已自动生成它。

      【讨论】:

        【解决方案8】:

        ModelMapper 库可用于此目的。

        public MyEntity clone(MyEntity myInstance) {
            MyEntity newInstance = new MyEntity();
            new ModelMapper().map(myInstance, newInstance);
            return newInstance;
        }
        

        只需添加 maven 依赖项

        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>2.3.2</version>
        </dependency>
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2016-03-17
          • 1970-01-01
          • 2019-01-21
          • 2014-06-16
          • 2018-02-05
          • 2010-11-30
          • 1970-01-01
          相关资源
          最近更新 更多