Inheritance and discriminator columns

Although it can be used for JOINED table inheritance, the @DiscriminatorValue is more common for SINGLE_TABLE inheritance. For SINGLE_TABLE, the discriminator column tells Hibernate the subclass entity type associated with each particular database row.

Without specifying a discriminator column, Hibernate is going to use the default DTYPE column. To visualize how it works, consider the following Domain Model inheritance hierarchy:

@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public static class Account {

    @Id
    private Long id;

    private String owner;

    private BigDecimal balance;

    private BigDecimal interestRate;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }

    public BigDecimal getBalance() {
        return balance;
    }

    public void setBalance(BigDecimal balance) {
        this.balance = balance;
    }

    public BigDecimal getInterestRate() {
        return interestRate;
    }

    public void setInterestRate(BigDecimal interestRate) {
        this.interestRate = interestRate;
    }
}

@Entity(name = "DebitAccount")
public static class DebitAccount extends Account {

    private BigDecimal overdraftFee;

    public BigDecimal getOverdraftFee() {
        return overdraftFee;
    }

    public void setOverdraftFee(BigDecimal overdraftFee) {
        this.overdraftFee = overdraftFee;
    }
}

@Entity(name = "CreditAccount")
public static class CreditAccount extends Account {

    private BigDecimal creditLimit;

    public BigDecimal getCreditLimit() {
        return creditLimit;
    }

    public void setCreditLimit(BigDecimal creditLimit) {
        this.creditLimit = creditLimit;
    }
}

For this mode, Hibernate generates the following database table:

create table Account (
    DTYPE varchar(31) not null,
    id bigint not null,
    balance decimal(19,2),
    interestRate decimal(19,2),
    owner varchar(255),
    overdraftFee decimal(19,2),
    creditLimit decimal(19,2),
    primary key (id)
)

So when inserting two subclass entities:

DebitAccount debitAccount = new DebitAccount();
debitAccount.setId( 1L );
debitAccount.setOwner( "John Doe" );
debitAccount.setBalance( BigDecimal.valueOf( 100 ) );
debitAccount.setInterestRate( BigDecimal.valueOf( 1.5d ) );
debitAccount.setOverdraftFee( BigDecimal.valueOf( 25 ) );

CreditAccount creditAccount = new CreditAccount();
creditAccount.setId( 2L );
creditAccount.setOwner( "John Doe" );
creditAccount.setBalance( BigDecimal.valueOf( 1000 ) );
creditAccount.setInterestRate( BigDecimal.valueOf( 1.9d ) );
creditAccount.setCreditLimit( BigDecimal.valueOf( 5000 ) );

Hibernate will populate the DTYPE column with the subclass class name:

INSERT INTO Account (balance, interestRate, owner, overdraftFee, DTYPE, id)
VALUES (100, 1.5, 'John Doe', 25, 'DebitAccount', 1)

INSERT INTO Account (balance, interestRate, owner, creditLimit, DTYPE, id)
VALUES (1000, 1.9, 'John Doe', 5000, 'CreditAccount', 2)

While this is rather straightforward for most use cases, when having to integrate a legacy database schema, it might be that the discriminator column contains NULL(s) or some values that are not associated to any entity subclass.

Consider that our database contains records like these:

INSERT INTO Account (DTYPE, balance, interestRate, owner, id)
VALUES (NULL, 300, 0.9, 'John Doe', 3)

INSERT INTO Account (DTYPE, active, balance, interestRate, owner, id)
VALUES ('Other', true, 25, 0.5, 'Johnny Doe', 4)

INSERT INTO Account (DTYPE, active, balance, interestRate, owner, id)
VALUES ('Unsupported', false, 35, 0.6, 'John Doe Jr.', 5)

With the previous mappings, when trying to fetch all Account(s):

Map<Long, Account> accounts = entityManager.createQuery(
        "select a from Account a", Account.class )
.getResultList()
.stream()
.collect( Collectors.toMap( Account::getId, Function.identity()));

We’d bump into the following kind of issues:

org.hibernate.WrongClassException: Object [id=3] was not of the specified subclass
[org.hibernate.userguide.inheritance.Account] : Discriminator: null

org.hibernate.WrongClassException: Object [id=4] was not of the specified subclass
[org.hibernate.userguide.inheritance.Account] : Discriminator: Other

org.hibernate.WrongClassException: Object [id=5] was not of the specified subclass
[org.hibernate.userguide.inheritance.Account] : Discriminator: Unsupported

Fortunately, Hibernate allows us to handle these mappings by using NULL and NOT NULL discriminator value mapping.

For the NULL values, we can annotate the base class Account entity as follows:

@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorValue( "null" )
public static class Account {

    @Id
    private Long id;

    private String owner;

    private BigDecimal balance;

    private BigDecimal interestRate;

    // Getter and setter omitted for brevity
}

For the Other and Unsupported discriminator values, we can have a miscellaneous entity that handles all values that were not explicitly mapped:

@Entity(name = "MiscAccount")
@DiscriminatorValue( "not null" )
public static class MiscAccount extends Account {

    private boolean active;

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }
}

This way, the aforementioned polymorphic query works and we can even validate the results:

assertEquals(5, accounts.size());
assertEquals( DebitAccount.class, accounts.get( 1L ).getClass() );
assertEquals( CreditAccount.class, accounts.get( 2L ).getClass() );
assertEquals( Account.class, accounts.get( 3L ).getClass() );
assertEquals( MiscAccount.class, accounts.get( 4L ).getClass() );
assertEquals( MiscAccount.class, accounts.get( 5L ).getClass() );

I have also updated the Hibernate 5.0, 5.1, and 5.2 documentations with these two very useful mapping options.


Back to top