How to inherit properties from a base class entity using @MappedSuperclass with JPA and Hibernate

Introduction

Last week, one of my blog readers asked me of a way to reuse the @Id mapping so that it won’t have to be declared on each an every entity.

Because this is a good opportunity to introduce @MappedSuperclass, I decided to answer the question with a blog post.

Domain Model

Assuming we are using MySQL and we have the following tables:

As I already explained, it’s a really bad idea to use the GenerationType.AUTO identifier strategy with MySQL because it falls back to using the sequence-like TABLE generator which scales badly.

So, even if the IDENTITY strategy prevents Hibernate from using JDBC batch inserts, it’s still a much better alternative to the TABLE identifier generation strategy.

However, 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(
        strategy = GenerationType.IDENTITY
    )
    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 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')

If you enjoyed this article, I bet you are going to love my book as well.

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.

Enter your email address to follow this blog and receive notifications of new posts by email.

Advertisements

7 thoughts on “How to inherit properties from a base class entity using @MappedSuperclass with JPA and Hibernate

  1. Regarding the @OneToOne maping using @MapsId:
    In this article in the PostDetails entity you use @JoinColumn:

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id")
    @MapsId
    private Post post;
    

    In the article mentioning @OneToOne you don’t use the @JoinColumn annotation: https://vladmihalcea.com/2016/07/26/the-best-way-to-map-a-onetoone-relationship-with-jpa-and-hibernate/

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

    Why? What is the difference?

  2. Nice article and I really enjoy reading your blog in general!

    One remark to the Tag entity: @NaturalId only works when the field is not inherited. At least from my testing over the last few hours… =)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s