The best way to map the @DiscriminatorColumn 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

As previously explained, the SINGLE_TABLE inheritance is the most efficient entity inheritance strategy.

However, for JPQL query such as this one:

List<Post> posts = entityManager
.createQuery(
    "select p " +
    "from Post p " +
    "where p.board = :board", Post.class)
.setParameter("board", board)
.getResultList();

Hibernate generates a SQL query which filters by the associated discriminator column (e.g. DTYPE by default):

SELECT t.id AS id2_1_,
       t.board_id AS board_id8_1_,
       t.createdOn AS createdO3_1_,
       t.owner AS owner4_1_,
       t.title AS title5_1_,
       t.content AS content6_1_
FROM   topic t
WHERE  t.DTYPE = 'Post'
       AND t.board_id = 1

So, because we are filtering by the discriminator column, we might want to index it or include it to speed up queries.

However, the default STRING DiscriminatorType expects a VARCHAR column that must hold the longest entity subclass name. For the Announcement class, we need at least 12 bytes to store the entity class name while for the Post entity, 4 bytes are required.

If the discriminator type column is indexed and we store 1 million Announcement and 100 million Post entities, the index will require 393 MB (12 + 400 million bytes). On the other hand, if the discriminator column is a TINYINT (only 1 byte is needed to store a discriminator value), we need only 96 MB (1 + 100 million bytes).

In this article, I’m going to explain how you can get the most out of the INTEGER DiscriminatorType while still retaining the descriptiveness of the default STRING DiscriminatorType.

Domain Model

Considering we have following entities in our system:

Class diagram

The Topic, Post, and Announcement class hierarchy is mapped onto a single table in the database:

Single table integer discriminator column

The Topic base class entity is mapped as follows:

@Entity(name = "Topic")
@Table(name = "topic")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
    discriminatorType = DiscriminatorType.INTEGER,
    name = "topic_type_id",
    columnDefinition = "TINYINT(1)"
)
public class Topic {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    private String owner;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdOn = new Date();

    @ManyToOne(fetch = FetchType.LAZY)
    private Board board;

    //Getters and setters omitted for brevity
}

Notice the @DiscriminatorColumn annotation which declares that an INTEGER discriminator is expected in the topic_type_id column which is of type TINYINT(1).

While for the default STRING discriminator, you don’t need to specify a certain value for each subclass entity since the class name is used, for INTEGER discriminator type, each subclass entity must provide a @DiscriminatorValue with a unique integer value.

The Post subclass will use the value of 1 in the topic_type_id column:

@Entity(name = "Post")
@Table(name = "post")
@DiscriminatorValue("1")
public class Post extends Topic {

    private String content;

    //Getters and setters omitted for brevity
}

While Announcement subclass will use the value of 2 in the topic_type_id column:

@Entity(name = "Announcement")
@Table(name = "announcement")
@DiscriminatorValue("2")
public class Announcement extends Topic {

    @Temporal(TemporalType.TIMESTAMP)
    private Date validUntil;

    //Getters and setters omitted for brevity
}

Testing time

When inserting one Post and an Announcement entity as well:

Post post = new Post();
post.setOwner("John Doe");
post.setTitle("Inheritance");
post.setContent("Best practices");
post.setBoard(board);

entityManager.persist(post);

Announcement announcement = new Announcement();
announcement.setOwner("John Doe");
announcement.setTitle("Release x.y.z.Final");
announcement.setValidUntil(
    Timestamp.valueOf(
        LocalDateTime.now().plusMonths(1)
    )
);
announcement.setBoard(board);

entityManager.persist(announcement);

Hibernate generates the following SQL statements:

Query:["insert into topic (board_id, createdOn, owner, title, content, topic_type_id, id) values (?, ?, ?, ?, ?, 1, ?)"], 
Params:[(1, 2017-06-02 16:30:35.963, John Doe, Inheritance, Best practices, 2)]

Query:["insert into topic (board_id, createdOn, owner, title, validUntil, topic_type_id, id) values (?, ?, ?, ?, ?, 2, ?)"], 
Params:[(1, 2017-06-02 16:30:35.974, John Doe, Release x.y.z.Final, 2017-07-02 16:30:35.98, 3)]

Notice the 1 and 2 literal values in the executed SQL INSERT statement.

Adding a description to each numeric value

Even if the INTEGER discriminator type is much more compact than its STRING counterpart, it nevertheless lacks expressivity because when you see a value of 2, you don’t automatically think that it represents an Announcement row.

So, how can we fix it?

Actually, the fix is simpler than you might think. What we need to do is to introduce a new table which holds a description for each numeric value, which will represent the table Primary Key.

Single table integer discriminator column with description

We can initialize the topic_type as follows:

INSERT INTO topic_type (
    description, 
    name, 
    id
) 
VALUES (
    'Post is a subclass of the Topic base class', 
    'com.vladmihalcea.book.hpjp.hibernate.inheritance.discriminator.Post', 
    1
)

INSERT INTO topic_type (
    description, 
    name, 
    id
) 
VALUES (
    'Announcement is a subclass of the Topic base class', 
    'com.vladmihalcea.book.hpjp.hibernate.inheritance.discriminator.Announcement', 
    2
)

Now, when we need to find the type of a given topic record, you just have to join it with the topic_type table:

SELECT 
    tt.name,
    t.id,
    t.createdOn,
    t.owner,
    t.title,
    t.content,
    t.validUntil,
    t.board_id
FROM topic t
INNER JOIN topic_type tt ON t.topic_type_id = tt.id

That’s it!

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

Like any column that needs to be indexed, the discriminator type is very important to application performance, and you should always choose the most compact type available.

Although the default STRING DiscriminatorType is very convenient, it’s much better to use an INTEGER discriminator type. To mitigate the lack of expressiveness, you can add a new table to hold the description for each particular discriminator type value.

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.