HHH-10966 - Document @DiscriminatorValue NULL and NOT_NULL options
This commit is contained in:
parent
cc94c57097
commit
c6ec57d714
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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[]
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue