The best way to clone or duplicate an entity with JPA and Hibernate

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

Have you ever wondered how to clone an entity with JPA or Hibernate? Recently, I stumbled upon this Hibernate forum question and it reminded me that this was a common requirement when working with JPA and Hibernate.

In this article, we are going to see the best way to clone a JPA entity with JPA and Hibernate.

Domain Model

Let’s assume we are using the following entities in our application:

The Tag entity is mapped as follows:

@Entity(name = "Tag")
@Table(name = "tag")
public class Tag {

    @Id
    private String name;

    //Getters and setters omitted for brevity
}

The Post entity has a many-to-many association with the Tag entity, and as explained in this article, it’s better to use a Set like this:

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

The PostDetails has a one-to-one association with the parent Post entity, and as explained in this article, the best way to map a one-to-one table relationship with JPA and Hibernate is to use the @MapsId annotation:

@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    @CreationTimestamp
    private Date createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;

    //Getters and setters omitted for brevity
}

The PostComment entity has a one-to-many association with the parent Post entity, and as explained in this article, the best way to map a one-to-many table relationship with JPA and Hibernate is to use the @ManyToOne annotation:

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

Test data

Now, let’s create some Tag entities first:

Tag java = new Tag();
java.setName("Java");

entityManager.persist(java);

Tag jdbc = new Tag();
jdbc.setName("JDBC");

entityManager.persist(jdbc);

Tag jpa = new Tag();
jpa.setName("JPA");

entityManager.persist(jpa);

Tag jooq = new Tag();
jooq.setName("jOOQ");

entityManager.persist(jooq);

And afterward, we can create a Post entity with a PostDetails child entity and 2 PostComment associated entities:

Post post = new Post();
post.setTitle(
	"High-Performance Java Persistence, 1st edition"
);

PostDetails details = new PostDetails();
details.setCreatedBy(
	"Vlad Mihalcea"
);
post.addDetails(details);

post.getTags().add(
	entityManager.getReference(Tag.class, "Java")
);
post.getTags().add(
	entityManager.getReference(Tag.class, "JDBC")
);
post.getTags().add(
	entityManager.getReference(Tag.class, "JPA")
);
post.getTags().add(
	entityManager.getReference(Tag.class, "jOOQ")
);

PostComment comment1 = new PostComment();
comment1.setReview(
	"This book is a big one"
);
post.addComment(comment1);

PostComment comment2 = new PostComment();
comment2.setReview(
	"5 stars"
);
post.addComment(comment2);

entityManager.persist(post);

Cloning the Post entity

Now, just like many blog platforms already offer, we want to have a feature where the user can duplicate a given Post so that it can use the previous Post as a template. This use case is a perfect candidate for entity cloning.

While a completely automated deep-cloning solution is what you might think of, in reality, we need more control over what’s to be cloned, and for this reason, it’s better to use a copy constructor approach instead like in the following example.

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

So, we are first fetching the previously published Post entity, and we want to use a new title while retaining some associations from the previous Post.

In order to achieve this goal, we need to add the following constructors in the Post entity:

/**
 * Needed by Hibernate when hydrating the entity 
 * from the JDBC ResultSet
 */
private Post() {}

public Post(Post post) {
    this.title = post.getTitle();

    addDetails(
        new PostDetails(post.getDetails())
    );

    tags.addAll(post.getTags());
}

The first constructor is the default one which we need to add because Hibernate makes use of it when instantiating a Post entity upon fetching it directly or via a query.

The second one is the copy constructor as it takes a Post entity to be used as a reference for building a new instance. Notice that we are copying the title, the details and the tags while leaving the comments empty. This makes sense because we don’t want to copy the user comments onto the newly published post. However, the tags might be relevant as well as the details association since it contains the user who published the post.

The PostDetails also features two constructors just like the Post entity:

/**
 * Needed by Hibernate when hydrating the entity
 * from the JDBC ResultSet
 */
private PostDetails() {
}

public PostDetails(PostDetails details) {
    this.createdBy = details.getCreatedBy();
}

The first constructor is the default one which is required by Hibernate and the second one is the copy constructor. Notice that we are only copying the createdBy attribute while leaving the createdOn attribute null as it will be initialized by Hibernate anyway since it’s annotated with the @CreationTimestamp annotation.

When executing the previous test case which clones the Post entity, Hibernate executes the following SQL INSERT queries:

SELECT p.id AS id1_0_0_,
       pd.post_id AS post_id3_2_1_,
       t.name AS name1_4_2_,
       p.title AS title2_0_0_,
       pd.created_by AS created_1_2_1_,
       pd.created_on AS created_2_2_1_,
       tags2_.post_id AS post_id1_3_0__,
       tags2_.tag_id AS tag_id2_3_0__
FROM post p
INNER JOIN post_details pd ON p.id=pd.post_id
INNER JOIN post_tag tags2_ ON p.id=tags2_.post_id
INNER JOIN tag t ON tags2_.tag_id=t.name
WHERE p.title = 'High-Performance Java Persistence, 1st edition'

CALL NEXT VALUE FOR hibernate_sequence

INSERT INTO post (title, id) 
VALUES ('High-Performance Java Persistence, 2nd edition', 4)

INSERT INTO post_details (created_by, created_on, post_id) 
VALUES ('Vlad Mihalcea', '2018-09-04 17:12:49.438', 4)

INSERT INTO post_tag (post_id, tag_id) VALUES (4, 'jOOQ')
INSERT INTO post_tag (post_id, tag_id) VALUES (4, 'JPA')
INSERT INTO post_tag (post_id, tag_id) VALUES (4, 'JDBC')
INSERT INTO post_tag (post_id, tag_id) VALUES (4, 'Java')

The SELECT statement fetches the Post entity along with the PostDetails and the Tag collection which we are referencing during cloning.

Next, the hibernate_sequence is called to assign a new identifier for the new Post entity.

The Post entity INSERT statement uses the new title while the PostDetails is inserted using the previous created_by column value.

All the Tag that were referenced by the previous Post entity are going to be associated with the new Post entity too.

Cool, right?

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

When cloning or duplicating an entity, using a copy constructor is the best way to control what properties and associations need to be retained by the cloned copy. Without explicitly choosing what needs to be cloned, subtle bugs may occur especially if bidirectional associations are not properly synchronized.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.