HHH-10966 - Document @DiscriminatorValue NULL and NOT_NULL options

This commit is contained in:
Vlad Mihalcea 2016-07-18 17:20:35 +03:00
parent cc94c57097
commit c6ec57d714
4 changed files with 475 additions and 10 deletions

View File

@ -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

View File

@ -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.
@ -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:
@ -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]
====
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.
====
[[entity-inheritance-discriminator-implicit]]
====== Implicit discriminator values
When using polymorphic queries, only a single table is required to be scanned to fetch all associated subclass instances.
Aside from the usual discriminator values assigned to each individual subclass type, the `@DiscriminatorValue` can take two additional values:
[[entity-inheritance-single-table-query-example]]
.Single Table polymorphic query
`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
====
[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-example,indent=0]
----
====
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.
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}/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

View File

@ -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[]
}

View File

@ -0,0 +1,197 @@
/*
* 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.annotations.DiscriminatorOptions;
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 DiscriminatorOptionsNotNullSingleTableTest 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" );
}
} );
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)"
);
}
} );
} );
doInJPA( this::entityManagerFactory, entityManager -> {
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() );
} );
}
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorOptions(force = true)
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( "Other" )
public static class OtherAccount extends Account {
private boolean active;
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
}