@DiscriminatorFormular – Modeling Single Table Inheritance Without a Discriminator

Inheritance is one of the key concepts of all object-oriented programming languages. And Java makes there no difference. All developers are familiar with this concept and expect to use it in all parts of their code. That, of course, also includes the persistence layer and the entity model. But the concept of inheritance doesn’t exist in relational table models. JPA and Hibernate bridge that gap by providing different inheritance mapping strategies that map the entities to one or more database tables.

The InheritanceType.SINGLE_TABLE is the default strategy and provides the best performance. It maps all entities of the inheritance hierarchy and their attributes to the same database table. Based on this mapping, your persistence provider can generate simple and efficient queries to fetch a specific subclass or all classes of the inheritance hierarchy.

Using this strategy introduces a technical requirement. For each database record, Hibernate needs to identify the subclass to which it has to map it. The default mapping uses a discriminator column, which contains a class-specific identifier. In most cases, that’s the simple name of the entity class.

But what do you do if you’re working with an existing table model that doesn’t contain such a column and that you’re not allowed to change? The JPA standard doesn’t provide a solution for this. It can’t use InheritanceType.SINGLE_TABLE without a discriminator column. But Hibernate does, if you can provide an SQL snippet that returns this information.

Domain Model

Let’s take a quick look at the domain model used in this article before diving into the mapping definition. The ChessTournament class is the superclass of the classes ChessSwissTournament and ChessRoundRobinTournament.

Class diagram: ChessTournament class with subclasses ChessSwissTournament and ChessRoundRobinTournament

As you can see in the diagram, the ChessTournament class defines almost all attributes. The ChessSwissTournament class adds the rounds attribute, and the ChessRoundRobinTournament class the numPlayers attribute.

Using the InheritanceType.SINGLE_TABLE, we will map all 3 classes to the ChessTournament table. It contains a column for each attribute of the 3 entity classes but no discriminator column.

Table model: chesstournament table with all columns mapped by the 3 entity classes

Defining a @DiscriminatorFormula

The discriminator-based mapping using InheritanceType.SINGLE_TABLE is straightforward. You annotate your superclass with @Entity and @Inheritance. Your subclasses extend the superclass, and you annotate them with @Entity. If you don’t want to use the simple class name as the discriminator value, you can define it using a @DiscriminatorValue annotation.

Without a discriminator column, you need to add a @DiscriminatorFormula annotation to the superclass and provide an SQL snippet that returns the discriminator value of a record. Everything else stays the same.

Here you can see the mapping of the ChessTournament class. I provide an SQL snippet to the @DiscriminatorFormula annotation, checking if the rounds attribute is not null. This check is based on the assumption that the rounds attribute of the ChessSwissTournament entity is mandatory. If the rounds attribute is not null the record represents a ChessSwissTournament. Otherwise, it’s a ChessRoundRobinTournament.

2 Ebooks 

to boost your Hibernate skills

Sign up below to join my newsletter and to get 2 of my best Hibernate ebooks:

  • Java 8 Support in Hibernate 5
  • Native Queries with Hibernate

I will collect, use and protect your data in accordance with my Privacy policy.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula("case when rounds is not null then 'Swiss' else 'RoundRobin' end")
public abstract class ChessTournament {
    
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tournament_seq")
    @SequenceGenerator(name = "tournament_seq", sequenceName = "tournament_seq", initialValue = 100)
    private Long id;

    private String name;

    private LocalDate startDate;

    private LocalDate endDate;

    @Version
    private int version;

    @ManyToMany
    private Set<ChessPlayer> players = new HashSet<>();

    @OneToMany
    private Set<ChessGame> games = new HashSet<>();
	
    // getter and setter methods
}

The Strings Swiss and RoundRobin returned by the SQL snippet of the @DiscriminatorFormula match the discriminator values defined for the ChessSwissTournament and ChessRoundRobinTournament entities.

@Entity
@DiscriminatorValue("Swiss")
public class ChessSwissTournament extends ChessTournament {
    
    private int rounds;
	
    // getter and setter methods
}
@Entity
@DiscriminatorValue("RoundRobin")
public class ChessRoundRobinTournament extends ChessTournament {
    
    private int numPlayers;
	
    // getter and setter methods
}

Fetching Entities

Let’s use a simple test case to try this mapping. I want to fetch the ChessTournament with id 1 from the database. Using JPA’s polymorphic query feature, I can select a ChessTournament entity, and Hibernate will return an object of the correct subclass. The tournament with id 1 is a ChessRoundRobinTournament entity.

@Test
public void testSample1() {
	log.info("==== test Sample 1 ====");

	EntityManager em = emf.createEntityManager();

	ChessTournament chessTournament = em.find(ChessTournament.class, 1L);

	log.info("==== Test Assertions ====");
	assertThat(chessTournament).isNotNull();
	assertThat(chessTournament instanceof ChessRoundRobinTournament).isTrue();
}

When running this test and activating my recommended logging configuration for development systems, you can see the executed SQL SELECT statement in the log output. Hibernate selects all columns mapped by the classes of the inheritance hierarchy and integrates the SQL snippet of the @DiscriminatorFormula annotation.

18:35:48,729 DEBUG SQL:144 - select chesstourn0_.id as id1_2_0_, chesstourn0_.endDate as enddate2_2_0_, chesstourn0_.name as name3_2_0_, chesstourn0_.startDate as startdat4_2_0_, chesstourn0_.version as version5_2_0_, chesstourn0_.numPlayers as numplaye6_2_0_, chesstourn0_.rounds as rounds7_2_0_, case when chesstourn0_.rounds is not null then 'Swiss' else 'RoundRobin' end as clazz_0_ from ChessTournament chesstourn0_ where chesstourn0_.id=?
18:35:48,731 TRACE BasicBinder:64 - binding parameter [1] as [BIGINT] - [1]
18:35:48,739 TRACE BasicExtractor:60 - extracted value ([clazz_0_] : [VARCHAR]) - [RoundRobin]
18:35:48,747 TRACE BasicExtractor:60 - extracted value ([enddate2_2_0_] : [DATE]) - [1953-10-24]
18:35:48,747 TRACE BasicExtractor:60 - extracted value ([name3_2_0_] : [VARCHAR]) - [Zurich international chess tournament]
18:35:48,747 TRACE BasicExtractor:60 - extracted value ([startdat4_2_0_] : [DATE]) - [1953-08-29]
18:35:48,748 TRACE BasicExtractor:60 - extracted value ([version5_2_0_] : [INTEGER]) - [0]
18:35:48,748 TRACE BasicExtractor:60 - extracted value ([numplaye6_2_0_] : [INTEGER]) - [15]

As you can see in the log output, SQL snippet returned the value RoundRobin and Hibernate mapped the record to a ChessRoundRobinTournament entity object.

Conclusion

When using an inheritance hierarchy in your domain model, Hibernate needs to apply a mapping strategy to map the classes to one or more database tables. By default, Hibernate uses InheritanceType.SINGLE_TABLE, which maps all entity classes of the inheritance hierarchy to the same database table.

This mapping requires a discriminator value that tells Hibernate to which subclass it has to map the record. By default, this value gets stored in a separate column. If your table model doesn’t provide such a column, you can use Hibernate’s @DiscriminatorFormula annotation. It expects an SQL snippet that returns the discriminator value for each record. Hibernate includes this snippet in the SELECT statement and maps the record based on the returned discriminator value when fetching an entity from the database.

Related Articles

Responses

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.

Black Friday & Launch Offer: Join the Persistence Hub!

Hi and welcome!

To make sure I provide you with the best content, please tell me what you are looking for.