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.
Tip - The best way to clone a JPA entity with JPA and #Hibernate @vlad_mihalceahttps://t.co/zBQrtO0s1c pic.twitter.com/VUlz7ylNBI
— Java (@java) September 8, 2018
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?
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.
