Imagine having a tool that can automatically detect JPA and Hibernate performance issues.
Hypersistence Optimizer is that tool!
Introduction
In this article, we are going to see how @MappedSuperclass can help us reuse the @Id mapping of a JPA and Hibernate entity so that it won’t have to be declared on each and every entity.
Domain Model
Assuming we are have the following tables:
We don’t want to declare the @Id on every entity (e.g. Post, PostDetails, PostComment, Tag), so let’s see how we can address this issue.
@MappedSuperclass
The JPA standard specification defines the @MappedSuperclass annotation to allow an entity to inherit properties from a base class.
Unlike the @Inheritance annotation which maps the Java Object inheritance to a relational database model which emulates inheritance, @MappedSuperclass only models inheritance in the OOP world.
From a database perspective, the @MappedSuperclass inheritance model is invisible since all the base class properties are simply copied to the database table mapped by the actual entity class.
Therefore, we can define the following BaseEntity base class:
@MappedSuperclass
public class BaseEntity {
@Id
@GeneratedValue
private Long id;
@Version
private Integer version;
//Getters and setters omitted for brevity
}
Now, our entities can extend the BasedEntity class and skip declaring the @Id or @Version properties since they are inherited from the base class. If the BaseEntity were not annotated with @MappedSuperclass, the @Id or @Version properties would not be inherited by the classes extending BasedEntity.
Post entity
@Entity(name = "Post")
@Table(name = "post")
public class Post extends BaseEntity {
private String title;
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List 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 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;
}
}
Also, we are using FetchType.LAZY because, by default, @OneToOne associations are fetched eagerly, and that’s bad for performance.
Tag entity
@Entity(name = "Tag")
@Table(name = "tag")
public class Tag extends BaseEntity {
@NaturalId
private String name;
//Getters and setters omitted for brevity
}
Note the use of the @NaturalId annotation which allows you to map a business key and fetch the Tag entity by its natural identifier.
Testing time
Now, when creating two Tag entities:
Tag jdbc = new Tag();
jdbc.setName("JDBC");
entityManager.persist(jdbc);
Tag hibernate = new Tag();
hibernate.setName("Hibernate");
entityManager.persist(hibernate);
Hibernate generates the following queries:
INSERT INTO tag (version, name)
VALUES (0, 'JDBC')
INSERT INTO tag (version, name)
VALUES (0, 'Hibernate')
Note that the version property is set because it’s inherited from the BaseEntity class.
We don’t need to provide the @Id because the IDENTITY strategy generates the entity identifier upon persisting the entity.
When saving a Post and its associated PostDetails entity:
Post post = new Post();
post.setTitle("High-Performance Java Persistence");
PostDetails postDetails = new PostDetails();
postDetails.setCreatedBy("Vlad Mihalcea");
postDetails.setCreatedOn(new Date());
post.addDetails(postDetails);
Session session = entityManager.unwrap(Session.class);
post.getTags().add(
session
.bySimpleNaturalId(Tag.class)
.getReference("jdbc")
);
post.getTags().add(
session
.bySimpleNaturalId(Tag.class)
.getReference("hibernate")
);
entityManager.persist(post);
Hibernate generates the following queries:
INSERT INTO post (version, title)
VALUES (0, 'High-Performance Java Persistence')
INSERT INTO post_details (version, created_by, created_on, id)
VALUES (0, 'Vlad Mihalcea', '2017-11-08 12:29:23.498', 1)
INSERT INTO post_tag (post_id, tag_id)
values (1, 2)
INSERT INTO post_tag (post_id, tag_id)
values (1, 1)
When saving a PostComment entity:
Post post = entityManager.createQuery(
"select p " +
"from Post p " +
"where p.title = :title", Post.class)
.setParameter("title", "High-Performance Java Persistence")
.getSingleResult();
PostComment postComment = new PostComment();
postComment.setReview("THE book on Hibernate");
post.addComment(postComment);
Hibernate generates the following queries:
INSERT INTO post_comment (version, post_id, review)
VALUES (0, 1, 'THE book on Hibernate')
So, every time you need to inherit properties from a base class, you need the @MappedSuperclass annotation. Otherwise, JPA entities will ignore the base class properties even if your entity extends a given base class.
As a rule of thumb, it’s not because you cannot override the @Id. It’s not much that you are gaining from this either. You can still make all classes implement an interface that exposes the id getter method though. It’s just the mapping is better to belong to the class itself.
Is it a good practice to inherit all entities ids in the application from one @MappedSuperClass?
As a rule of thumb, it’s not because you cannot override the
@Id
. It’s not much that you are gaining from this either. You can still make all classes implement an interface that exposes the id getter method though. It’s just the mapping is better to belong to the class itself.