32

I'm using a relational DB using a single column pk with a few nested tables.

I need to add a simple archiving to my project. The archiving only happens when the application reaches a particular state, so what I was hoping to do was copy my existing hibernate object into a new instance where the new instance would be saved with a new ID while leaving the existing object intact.

I can't seem to figure out how to copy the existing object into a new instance without manually having to set every single new instance field.

Does anybody know of a simple way of doing this?

Vlad Mihalcea
  • 103,297
  • 39
  • 432
  • 788
Code Junkie
  • 6,772
  • 22
  • 69
  • 127

9 Answers9

47

Just retrieve the object, detach it, set the id to null and persist it.

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

If your object have oneToMany relationships, you will have to repeat the operation for all the children but setting your parent object id (generated after the persist call) instead of null.

Of course you will have to remove any CASCADE persist on your OneToMany relationships cause otherwise your persist will create duplicates of all children in DB or fk constraint failures.

Rubens Mariuzzo
  • 25,735
  • 25
  • 111
  • 145
Gab
  • 7,071
  • 2
  • 32
  • 62
  • I expected this approach to work but .... surprise, surprise I an exception org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke() when trying to re-set the id – Legna Jun 24 '15 at 12:51
  • 1
    on a child ? Did you detach it before ? (detach is not recursive unless with cascadeType=DETACH) – Gab Jun 24 '15 at 12:58
  • 2
    @Legna That means that it is trying to work on something that wasn't lazy loaded. initialize the children you need to clone first. [See this link](http://stackoverflow.com/a/19928738/2241785) for details on how to do that. – S. Buda Oct 02 '15 at 17:10
9

Using detach or deep cloning as suggested by others is not the way to go when it comes to cloning an entity. If you try to make this process completely automatic, you are going to miss the point that not all attributes are worth duplicating.

Therefore, you are better off using a copy constructor and controlling exactly what attributes need to be cloned.

So, if you have a Post entity like this one:

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

It does not make sense to clone the comments when duplicating a Post and using it as a template for a new one:

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);

What you need to add to the Post entity is a 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());
}

So, the copy constructor is the best way to address the entity clone/duplication problem.

Vlad Mihalcea
  • 103,297
  • 39
  • 432
  • 788
9

I am also working with Hibernate and I got the same requirement you got. What I followed was to implement Cloneable. Below is a code example of how to do it.

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

Or you could go to a reflection based solution but I won't recommend that. Check this website for more details.

Chan
  • 2,473
  • 5
  • 22
  • 43
  • 3
    How do you handle the nested tables such as oneToMany's etc? I was afraid I'd have to set all the object variables again. :( – Code Junkie Mar 30 '12 at 14:58
  • 1
    My case was not about Nested objects so I didn't face that problem... I am afraid you'll have to deep copy... There is always generation that can be done... – Chan Mar 30 '12 at 15:24
  • 1
    Usage of clone is not recommended, ref: http://www.javapractices.com/topic/TopicAction.do?Id=71 – Thor Hovden Sep 18 '13 at 12:50
  • .. so while copy constructors don't do well when using Hibernate, I'd go for a static factory method. – Thor Hovden Sep 19 '13 at 09:35
3

Have a look at following link. one of the most powerful cloning mechanism which can be utilized in most effective fashion with hibernate

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

ParkerM
  • 134
  • 1
  • 1
  • 9
2

You could clone the object and wipe the id and save it back down so a new Id is assigned.

so

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

in this case the createCopyWithNoId() would create a clone of the object and set the Id field to null. when you now add the new clone the hibernate engine will see it as a new object persist it and the database will assign a new primary key.

Please notice i avoided calling the method clone because what comes out is not an exact clone as we manipulate the Id (setting it null);

Another way to go would be to add a constructor that took in an object of type person and created a new object but simply didn't set the Id field leaving it at its default value of null. Again you persist using the DAO.

krystan honour
  • 5,934
  • 2
  • 31
  • 61
  • 1
    Could you elaborate a little bit more on this. I'm not entirely sure what createCopyWithNoId is. I think that's where I need clarification. Also, could this perform a deep clone? – Code Junkie Mar 30 '12 at 15:00
1

1- Add commons-lang dependency to pom.xml file

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

2- Import org.apache.commons.lang.SerializationUtils

3- Use SerializationUtils.clone(oldObj)

ex. Class1 newObj = (Class1) SerializationUtils.clone(oldObj);

see also java-deep-copy

Waleed Emad
  • 91
  • 1
  • 8
0

You could use mapstruct and create a method copy from object to object while ignoring id and every field that you don't want to copy.

If you have asscociations like one to many, many to many etc. you must have also a copy method in their mappers.

For binding children with parents, you can use afterMapping annotation and have add methods like Vlad Mihalcea has posted and do your work there or just do it in a service

0

We had got similar requirement, in which we need to copy entity at certain point of transaction and use it for Audit. Solution is pretty simple. We can use Json Object Mapper.

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);

Another option is to use In-house Spring Utils.

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

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

Note: When using BeanUtils, Bean will be tracked in transaction. i.e. if any changes done is source bean during transaction will be reflected in target copied bean.

user1480133
  • 167
  • 1
  • 6
0

Either you can clone if the object is clonable or you can define a method/constructor for the object you want to copy taking a parameter of itself and copying everthing you need, into a new instance and returning it to you.

fmucar
  • 13,463
  • 2
  • 42
  • 50