Skip to content

Commit

Permalink
HHH-10966 - Document @DiscriminatorValue NULL and NOT_NULL options
Browse files Browse the repository at this point in the history
  • Loading branch information
vladmihalcea committed Jul 18, 2016
1 parent 3e5947e commit 1b83be8
Show file tree
Hide file tree
Showing 4 changed files with 473 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
INSERT INTO Account (balance, interestRate, owner, overdraftFee, DTYPE, id)
VALUES (100, 1.5, 'John Doe', 25, 'Debit', 1)

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

INSERT INTO Account (balance, interestRate, owner, id)
VALUES (1000, 1.9, 'John Doe', 3)

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

SELECT a.id as id2_0_,
a.balance as balance3_0_,
a.interestRate as interest4_0_,
a.owner as owner5_0_,
a.overdraftFee as overdraf6_0_,
a.creditLimit as creditLi7_0_,
a.active as active8_0_,
a.DTYPE as DTYPE1_0_
FROM Account a
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,29 @@ include::{extrasdir}/entity-inheritance-single-table-persist-example.sql[]
----
====

When using polymorphic queries, only a single table is required to be scanned to fetch all associated subclass instances.

[[entity-inheritance-single-table-query-example]]
.Single Table polymorphic query
====
[source,java]
----
include::{sourcedir}/SingleTableTest.java[tags=entity-inheritance-single-table-query-example,indent=0]
----
[source,sql]
----
include::{extrasdir}/entity-inheritance-single-table-query-example.sql[]
----
====

[IMPORTANT]
====
Among all other inheritance alternatives, the single table strategy performs the best since it requires access to one table only.
Because all subclass columns are stored in a single table, it's not possible to use NOT NULL constraints anymore, so integrity checks must be moved either into the data access layer or enforced through `CHECK` or `TRIGGER` constraints.
====

[[entity-inheritance-discriminator]]
===== Discriminator

The discriminator column contains marker values that tell the persistence layer what subclass to instantiate for a particular row.
Expand Down Expand Up @@ -112,6 +135,9 @@ Usually, the column should be part of the INSERT statement, but if your discrimi
There used to be `@org.hibernate.annotations.ForceDiscriminator` annotation which was deprecated in version 3.6 and later removed. Use `@DiscriminatorOptions` instead.
====

[[entity-inheritance-discriminator-formula]]
====== Discriminator formula

Assuming a legacy database schema where the discriminator is based on inspecting a certain column,
we can take advantage of the Hibernate specific `@DiscriminatorFormula` annotation and map the inheritance model as follows:

Expand All @@ -132,28 +158,48 @@ include::{extrasdir}/entity-inheritance-single-table-discriminator-formula-examp
The `@DiscriminatorFormula` defines a custom SQL clause that can be used to identify a certain subclass type.
The `@DiscriminatorValue` defines the mapping between the result of the `@DiscriminatorFormula` and the inheritance subclass type.

[IMPORTANT]
[[entity-inheritance-discriminator-implicit]]
====== Implicit discriminator values

Aside from the usual discriminator values assigned to each individual subclass type, the `@DiscriminatorValue` can take two additional values:

`null`:: If the underlying discriminator column is null, the `null` discriminator mapping is going to be used.
`not null`:: If the underlying discriminator column has a not-null value that is not explicitly mapped to any entity, the `not-null` discriminator mapping used.

To understand how these two values work, consider the following entity mapping:

[[entity-inheritance-single-table-discriminator-value-example]]
.@DiscriminatorValue `null` and `not-null` entity mapping
====
Among all other inheritance alternatives, the single table strategy performs the best since it requires access to one table only.
Because all subclass columns are stored in a single table, it's not possible to use NOT NULL constraints anymore, so integrity checks must be moved into the data access layer.
[source,java]
----
include::{sourcedir}/DiscriminatorNotNullSingleTableTest.java[tags=entity-inheritance-single-table-discriminator-value-example,indent=0]
----
====

When using polymorphic queries, only a single table is required to be scanned to fetch all associated subclass instances.
The `Account` class has a `@DiscriminatorValue( "null" )` mapping, meaning that any `account` row which does not contain any discriminator value will be mapped to an `Account` base class entity.
The `DebitAccount` and `CreditAccount` entities use explicit discriminator values.
The `OtherAccount` entity is used as a generic account type because it maps any database row whose discriminator column is not explicitly assigned to any other entity in the current inheritance tree.

[[entity-inheritance-single-table-query-example]]
.Single Table polymorphic query
To visualize how it works, consider the following example:

[[entity-inheritance-single-table-discriminator-value-persist-example]]
.@DiscriminatorValue `null` and `not-null` entity persistence
====
[source,java]
----
include::{sourcedir}/SingleTableTest.java[tags=entity-inheritance-single-table-query-example,indent=0]
include::{sourcedir}/DiscriminatorNotNullSingleTableTest.java[tags=entity-inheritance-single-table-discriminator-value-persist-example,indent=0]
----
[source,sql]
----
include::{extrasdir}/entity-inheritance-single-table-query-example.sql[]
include::{extrasdir}/entity-inheritance-single-table-discriminator-value-persist-example.sql[]
----
====

As you can see, the `Account` entity row has a value of `NULL` in the `DTYPE` discriminator column,
while the `OtherAccount` entity was saved with a `DTYPE` column value of `other` which has not explicit mapping.

[[entity-inheritance-joined-table]]
==== Joined table

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.userguide.inheritance;

import java.math.BigDecimal;
import java.sql.Statement;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;

import org.hibernate.Session;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;

import org.hibernate.testing.RequiresDialect;
import org.junit.Test;

import static org.hibernate.userguide.util.TransactionUtil.doInJPA;
import static org.junit.Assert.assertEquals;

/**
* @author Vlad Mihalcea
*/
@RequiresDialect( H2Dialect.class )
public class DiscriminatorNotNullSingleTableTest extends BaseEntityManagerFunctionalTestCase {

@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {
DebitAccount.class,
CreditAccount.class,
OtherAccount.class
};
}

@Test
public void test() {
doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.unwrap( Session.class ).doWork( connection -> {
try(Statement statement = connection.createStatement()) {
statement.executeUpdate( "ALTER TABLE Account ALTER COLUMN DTYPE SET NULL" );
}
} );

//tag::entity-inheritance-single-table-discriminator-value-persist-example[]
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 ) );

Account account = new Account();
account.setId( 3L );
account.setOwner( "John Doe" );
account.setBalance( BigDecimal.valueOf( 1000 ) );
account.setInterestRate( BigDecimal.valueOf( 1.9d ) );

entityManager.persist( debitAccount );
entityManager.persist( creditAccount );
entityManager.persist( account );

entityManager.unwrap( Session.class ).doWork( connection -> {
try(Statement statement = connection.createStatement()) {
statement.executeUpdate(
"insert into Account (DTYPE, active, balance, interestRate, owner, id) " +
"values ('Other', true, 25, 0.5, 'Vlad', 4)"
);
}
} );
//end::entity-inheritance-single-table-discriminator-value-persist-example[]
} );

doInJPA( this::entityManagerFactory, entityManager -> {
//tag::entity-inheritance-single-table-discriminator-value-persist-example[]

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

assertEquals(4, accounts.size());
assertEquals( DebitAccount.class, accounts.get( 1L ).getClass() );
assertEquals( CreditAccount.class, accounts.get( 2L ).getClass() );
assertEquals( Account.class, accounts.get( 3L ).getClass() );
assertEquals( OtherAccount.class, accounts.get( 4L ).getClass() );
//end::entity-inheritance-single-table-discriminator-value-persist-example[]
} );
}

//tag::entity-inheritance-single-table-discriminator-value-example[]
@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;

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")
@DiscriminatorValue( "Debit" )
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")
@DiscriminatorValue( "Credit" )
public static class CreditAccount extends Account {

private BigDecimal creditLimit;

public BigDecimal getCreditLimit() {
return creditLimit;
}

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

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

private boolean active;

public boolean isActive() {
return active;
}

public void setActive(boolean active) {
this.active = active;
}
}
//end::entity-inheritance-single-table-discriminator-value-example[]
}
Loading

0 comments on commit 1b83be8

Please sign in to comment.