How to inherit properties from a base class entity using @MappedSuperclass 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
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; } }
Note that we are using mappedBy
@OneToMany
associations because this is the best way to map this type of relationship.
Also, the @ManyToMany
association uses Set
instead of List
because that’s going to yield more efficient queries for this type of relationship.
PostComment entity
@Entity(name = "PostComment") @Table(name = "post_comment") public class PostComment extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private Post post; private String review; //Getters and setters omitted for brevity }
Note that we are using FetchType.LAZY
because, by default, @ManyToOne
associations are fetched eagerly, and that’s bad for performance.
PostDetails entity
@Entity(name = "PostDetails") @Table(name = "post_details") public class PostDetails extends BaseEntity { @Column(name = "created_on") private Date createdOn; @Column(name = "created_by") private String createdBy; @OneToOne(fetch = FetchType.LAZY) @MapsId private Post post; //Getters and setters omitted for brevity }
Note that we are using @MapsId
which is the best way to map a @OneToOne
association.
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 theIDENTITY
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')
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
And there is more!
You can earn a significant passive income stream from promoting all these amazing products that I have been creating.
If you're interested in supplementing your income, then join my affiliate program.
Conclusion
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.
