Hibernate Inheritance: Choosing a Strategy
Inheritance is one of the main principles of OOP. At the same time, a significant number of enterprise applications are based on relational databases.
The main contradiction between the object-oriented and relational models is that the object model supports two types of relations (“is a” - “is” and “has a” - “has”), and SQL-based models only support “has a” relationships.
In other words, SQL does not understand type inheritance and does not support it .
Therefore, at the stage of constructing entities and database schemas, one of the main tasks of the developer will be the choice of the optimal strategy for representing the inheritance hierarchy.
Total of such strategies 4:
1)Use one table for each class and default polymorphic behavior.
2) One table for each particular class, with the complete exclusion of polymorphism and inheritance relations from the SQL schema (UNION queries will be used for polymorphic behavior at runtime)
3) A single table for the entire class hierarchy. Only possible by denormalizing the SQL schema. It will be possible to define a superclass and subclasses by distinguishing strings.
4) One table for each subclass, where the relation “is a” is represented as “has a”, i.e. - Foreign key communication using JOIN.
There are 3 main factors that will be affected by your chosen strategy:
1)Performance (we use “hibernate_show_sql” to see and evaluate all queries made to the database)
2) Normalization of the scheme and guarantee of data integrity (not every strategy guarantees compliance with the NOT NULL constraint)
3) The possibility of evolution of your scheme
Under cat, each of these strategies will be considered in detail, indicating the advantages and disadvantages, as well as recommendations will be given on choosing a strategy in specific cases.
Situation:
We decided to overshadow the glory of eBay and are creating for this purpose our online auction application. Each User can place bets, and in the event that his rate is the largest - make payment online.
Actually, we will consider the payment process as a data model.
User can make a payment in two ways: using a bank card, or through bank account details.
The class diagram is presented below:
The BankAccount and CreditCard classes inherit from the common abstract ancestor of BillingDetails. As can be seen from the diagram, despite the similar functionality, their states differ significantly: for the card, the number and validity are important to us, and for the bank account, the details fields.
The parent class stores only owner information common to all descendants.
In addition, one can take out, for example, the Id field together with the type of generation (in this case, we did without it).
The schema of our database for the first strategy will look like this:
Queries for creating tables:
Polymorphism in this case will be implicit. Each child class we can reflect with the Entity annotation .
IMPORTANT! Superclass properties will be ignored by default. To save them to a table of a specific subclass, you must use the @MappedSuperClass annotation.
Displaying subclasses is not unusual. The only thing you should pay attention to is the @AttributeOverride annotation, perhaps unfamiliar to some.
It is used to rename a column in a subclass table if the names of the ancestor and the descendant table do not match (in our case, so that the “owner” from BillingDetails maps to CC_OWNER in the CREDIT_CARD table).
The main problem with using this strategy is that it will be impossible to use polymorphic associations to the full extent: they are usually presented in the database as foreign key access, and we simply do not have the BILLING_DETAILS table. And since each BillingDetails object in the application will be associated with a specific User object, then each of the descendant tables will need a foreign key that refers to the USERS table.
In addition, polymorphic queries will also be a problem.
Let's try to fulfill the request
To do this (hereinafter) just run the main () method.
In this case, it will be performed as follows:
In other words, for each particular subclass, Hibernate uses a separate SELECT query.
Another important issue when using this strategy will be the complexity of refactoring. Changing the name of the fields in the superclass will cause the need to change the names in many tables and require manual renaming (most IDE tools do not take into account @AttributeOverride). If your scheme does not have 2 tables, but 50, this is fraught with a lot of time.
This approach can be used only at the top of the class hierarchy, where:
a) Polymorphism is not needed (Hibernate will perform a selection for a particular subclass in one request -> performance will be high)
b) No changes are expected in the superclass.
For an application where requests will reference the BillingDetails parent class, this strategy is not suitable.
The role of the abstract class will again be BillingDetails.
The database schema will also remain unchanged.
The only thing is that the CC_OWNER field in the CREDIT_CARD table will have to be renamed OWNER, since this strategy does not support @AttributeOverride. From the documentation :
"The limitation of this approach is that if a property is mapped on the superclass, the column name must be the same on all subclass tables."
The annotation @Inheritance indicated above the superclass with the indicated TABLE_PER_CLASS strategy will also be new.
IMPORTANT! Within the framework of this strategy, the presence of an identifier in a superclass is a mandatory requirement (in the first example, we did without it).
IMPORTANT! According to the JPA standard, the TABLE_PER_CLASS strategy is optional, therefore, other implementations may not be supported.
Our SQL schema still knows nothing about inheritance; there is no relationship between tables.
The main advantage of this strategy can be seen by executing the polymorphic query from the previous example.
This time it will be executed differently:
In this case, Hibernate uses FROM to retrieve all instances of BillingDetails from all subclass tables. Tables are joined using UNION, and literals (1 and 2) are added to the intermediate result. Literals are used by Hibernate to instantiate the correct class.
Combining tables requires the same column structure, so NULL was inserted instead of non-existing columns (for example, "null :: varchar as bank_name" in credit_card - there is no bank name in the credit card table).
Another important advantage over the first strategy is the ability to use polymorphic associations. Now it will be possible to display associations between the User and BillingDetails classes without any problems.
The class hierarchy can be completely selected in one table. It will contain columns for all fields of each class in the hierarchy. For each record, a particular subclass will be determined by the value of an additional column with a selector .
Our diagram now looks like this:
Structure of Java classes:
To create a mapping with a single table, you must use the inheritance strategy SINGLE_TABLE.
The root class will be mapped to table BILLING_DETAILS. To distinguish between types, a selector column will be used. It is not an entity field and was created only for the needs of Hibernate. Its value will be strings - “CC” or “BA”.
IMPORTANT! If you do not explicitly specify the selector column in the superclass, it will get the default name DTYPE and type VARCHAR.
Each hierarchy class can indicate its selector value using the @DiscriminatorValue annotation.
Do not neglect the explicit name of the selector: by default, Hibernate will use the fully qualified class name or entity name (it depends on whether XML-Hibernate files or JPA / annotation xml files are used).
For verification, we use the familiar request in the main method
In the case of a single table, this query will be executed as follows:
If the request is executed to a specific subclass, the line “where BD_TYPE =“ CC ”” will be simply added.
Here's what the mapping into a single table will look like:
In the case where the scheme has been inherited and it is impossible to add a selector column to it, the @DiscriminatorFormula annotation comes to the rescue, which must be added to the parent class. It is necessary to pass the expression CASE ... WHEN into it.
The main advantage of this strategy is performance. Queries (both polymorphic and non-polymorphic) are very fast and can be easily written manually. It is not necessary to use connections and unions. The evolution of the circuit is also very simple.
However, the problems surrounding this strategy will often outweigh its benefits.
The main one is data integrity. Columns of those properties that are declared in subclasses may contain NULL. As a result, a simple software error can lead to a credit card with no number or expiration date in the database.
Another problem will be a violation of normalization, and in particular of the third normal form. In this light, the benefits of increased productivity already look doubtful. After all, you will have to at least sacrifice the convenience of escort: in the long term, denormalized schemes do not bode well.
The scheme of our classes will remain unchanged:
But in the database scheme, some changes have occurred
In Java code, you must use the JOINED strategy to create such a mapping.
Now, when saving, for example, an instance of CreditCard, Hibernate will insert two records. The properties declared in the fields of the BillingDetails superclass will fall into the BILLING_DETAILS table, and the values of the fields of the CreaditCard subclass will be written to the CREDIT_CARD table. These entries will be combined by a common primary key.
Thus, the circuit was brought to normal. The evolution of the circuit and the determination of integrity constraints are also simple.
Foreign keys allow you to represent a polymorphic association with a particular subclass.
By completing the request
, we will see the following picture:
BILLING_DETAILS
CREDIT_CARD
BANK_ACCOUNT
The CASE ... WHEN clause allows Hibernate to define a specific subclass for each record. It checks for the presence or absence of rows in the tables of subclasses CREDIR_CARD and BANK_ACCOUNT using literals.
Such a strategy will be very difficult to implement manually. Even implementing reports based on arbitrary queries will be much more difficult.
Performance may also be unacceptable for a particular project, because queries will require joining multiple tables or many sequential read operations.
When working with the strategies TABLE_PER_CLASS, SINGLE_TABLE and JOINED, a significant inconvenience is the fact that it is impossible to switch between them. You will have to adhere to the chosen strategy until the end (or completely change the scheme).
But there are tricks with which you can switch the display strategy for a particular subclass.
For example, displaying the class hierarchy in a single table (strategy 3), you can select a strategy with a separate table and foreign key for a separate subclass (strategy 4).
Now we can map the CreditCard subclass into a separate table.
To do this, we will need to apply the InheritanceType.SINGLE_TABLE strategy to the BillingDetails superclass, and the @SecondaryTable annotation will help us in working with the CreditCard class.
Using the annotations @SecondaryTable and @Column, we redefine the main table and its columns, indicating to Hibernate where to get the data from.
When you select the SINGLE_TABLE strategy, subclass columns may contain NULL. Using this technique, you can guarantee data integrity for a particular subclass (in our case, CreditCard).
By executing a polymorphic query, Hibernate will perform an outer join to retrieve instances of BillingDetails and all its subclasses.
Let's try:
Result:
This technique can be applied to other classes of the hierarchy, but for an extensive hierarchy, it does not work too well, since external connection in this case will become a problem. For such a hierarchy, a strategy that will immediately execute a second SQL query instead of an external join is best suited.
Each of the above strategies and techniques has its advantages and disadvantages. General recommendations for choosing a specific strategy will look like this:
- Strategy number 2 (TABLE_PER_CLASS based on UNION), if polymorphic queries and associations are not required. If you rarely (or don’t execute at all) “select bd from BillingDetails bd” and you don’t have classes that reference BillingDetails, this option would be better (since you will still be able to add optimized polymorphic queries and associations).
- Strategy number 3 (SINGLE_TABLE) should be used:
a) Only for simple tasks. In situations where normalization and NOT NULL constraint are critical, strategy # 4 (JOINED) should be preferred. It makes sense to think about whether it is worthwhile to abandon inheritance altogether and replace it with delegation
b) If polymorphic queries and associations are required, as well as the dynamic definition of a particular class at run time; however, subclasses declare relatively few new fields, and the main difference with the superclass is the behavior.
Well, in addition to this, you have a serious conversation with the DBA.
- Strategy # 4 (JOINED) is suitable in cases where polymorphic queries and associations are required, but subclasses declare relatively many new fields.
It is worth mentioning here: the decision between JOINED and TABLE_PER_CLASS requires evaluating plans for query execution on real data, since the width and depth of the inheritance hierarchy can make the cost of connections (and, as a result, performance) unacceptable.
We should also take into account that inheritance annotations cannot be applied to interfaces.
Thanks for attention!
The main contradiction between the object-oriented and relational models is that the object model supports two types of relations (“is a” - “is” and “has a” - “has”), and SQL-based models only support “has a” relationships.
In other words, SQL does not understand type inheritance and does not support it .
Therefore, at the stage of constructing entities and database schemas, one of the main tasks of the developer will be the choice of the optimal strategy for representing the inheritance hierarchy.
Total of such strategies 4:
1)Use one table for each class and default polymorphic behavior.
2) One table for each particular class, with the complete exclusion of polymorphism and inheritance relations from the SQL schema (UNION queries will be used for polymorphic behavior at runtime)
3) A single table for the entire class hierarchy. Only possible by denormalizing the SQL schema. It will be possible to define a superclass and subclasses by distinguishing strings.
4) One table for each subclass, where the relation “is a” is represented as “has a”, i.e. - Foreign key communication using JOIN.
There are 3 main factors that will be affected by your chosen strategy:
1)Performance (we use “hibernate_show_sql” to see and evaluate all queries made to the database)
2) Normalization of the scheme and guarantee of data integrity (not every strategy guarantees compliance with the NOT NULL constraint)
3) The possibility of evolution of your scheme
Under cat, each of these strategies will be considered in detail, indicating the advantages and disadvantages, as well as recommendations will be given on choosing a strategy in specific cases.
A few words from the author and instructions for working with examples
This article is an extract from the book Java Persistance with Hibernate. Its authors - the founder of the Hibernate project Gavin King (Gavin King) and a member of the Hibernate development team Christian Bauer (Christian Bauer).
In the summer of 2017, it was translated and published in Russian.
I tried to simplify the presentation of the material, as well as working with examples. Feeling a strong dislike for the examples that you need to mess with for an hour to start, I tried to make working with them as convenient as possible in this article:
- You can simply copy all the Java code into your IDE. All changes to the Java code when moving from one strategy to another are indicated in the spoilers, so when switching to a new strategy, the old class code can simply be deleted and the new one copied. The Main and HibernateUtil classes will remain unchanged, and will work when considering all the examples.
- In the spoilers for each strategy, you will also find scripts for creating all database tables. Therefore, after you have examined the next strategy, you can simply drop all the tables - in the next section you will find the actual scripts for creating new ones.
Code written using Java 1.7, Hibernate5 and PostgreSQL9.
Enjoy reading!
In the summer of 2017, it was translated and published in Russian.
I tried to simplify the presentation of the material, as well as working with examples. Feeling a strong dislike for the examples that you need to mess with for an hour to start, I tried to make working with them as convenient as possible in this article:
- You can simply copy all the Java code into your IDE. All changes to the Java code when moving from one strategy to another are indicated in the spoilers, so when switching to a new strategy, the old class code can simply be deleted and the new one copied. The Main and HibernateUtil classes will remain unchanged, and will work when considering all the examples.
- In the spoilers for each strategy, you will also find scripts for creating all database tables. Therefore, after you have examined the next strategy, you can simply drop all the tables - in the next section you will find the actual scripts for creating new ones.
Code written using Java 1.7, Hibernate5 and PostgreSQL9.
Enjoy reading!
Strategy 1
One table for each class
Situation:
We decided to overshadow the glory of eBay and are creating for this purpose our online auction application. Each User can place bets, and in the event that his rate is the largest - make payment online.
Actually, we will consider the payment process as a data model.
User can make a payment in two ways: using a bank card, or through bank account details.
The class diagram is presented below:
Java code to run the example
pom.xml:
hibernate.cfg.xml
Main class with main () method:
4.0.0 com.hiber.jd2050 hiberLearn 1.0-SNAPSHOT UTF-8 1.7 org.apache.maven.plugins maven-compiler-plugin 3.1 ${java.version} postgresql postgresql 9.0-801.jdbc4 org.hibernate.javax.persistence hibernate-jpa-2.1-api 1.0.0.Final javax.transaction jta 1.1 org.hibernate hibernate-core 5.0.5.Final
hibernate.cfg.xml
jdbc:postgresql://localhost:5432/dobrynin_db org.postgresql.Driver postgres filyaSl9999 org.hibernate.dialect.PostgreSQL9Dialect create-drop true true thread
import javax.persistence.*;
@MappedSuperclass
public abstract class BillingDetails {
private String owner;
public BillingDetails() {
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
@Override
public String toString() {
return "BillingDetails{" +
"owner='" + owner + '\'' +
'}';
}
}
import javax.persistence.*;
@Entity
@Table(name = "CREDIT_CARD")
public class CreditCard extends BillingDetails {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
@Column(name = "card_number")
private int cardNumber;
@Column(name = "exp_month")
private String expMonth;
@Column (name = "exp_year")
private String expYear;
public CreditCard() {
}
public int getCardNumber() {
return cardNumber;
}
public String getExpMonth() {
return expMonth;
}
public String getExpYear() {
return expYear;
}
public void setCardNumber(int cardNumber) {
this.cardNumber = cardNumber;
}
public void setExpMonth(String expMonth) {
this.expMonth = expMonth;
}
public void setExpYear(String expYear) {
this.expYear = expYear;
}
@Override
public String toString() {
return "CreditCard{" +
"cardNumber=" + cardNumber +
", expMonth='" + expMonth + '\'' +
", expYear='" + expYear + '\'' +
'}';
}
}
import javax.persistence.*;
@Entity
@Table(name = "BANK_ACCOUNT")
public class BankAccount extends BillingDetails {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
private int account;
@Column(name = "bank_name")
private String bankName;
private String swift;
public BankAccount() {
}
public int getAccount() {
return account;
}
public void setAccount(int account) {
this.account = account;
}
public String getBankName() {
return bankName;
}
public void setBankName(String bankName) {
this.bankName = bankName;
}
public String getSwift() {
return swift;
}
public void setSwift(String swift) {
this.swift = swift;
}
@Override
public String toString() {
return "BankAccount{" +
"account=" + account +
", bankName='" + bankName + '\'' +
", swift='" + swift + '\'' +
'}';
}
}
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class HibernateUtil {
public static SessionFactory getSessionFactory() {
return new Configuration().configure().buildSessionFactory();
}
}
Main class with main () method:
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception {
CreditCard creditCard = new CreditCard();
creditCard.setCardNumber(44411111);
creditCard.setExpMonth("Jan");
creditCard.setExpYear("2017");
creditCard.setOwner("Bill Gates");
BankAccount bankAccount = new BankAccount();
bankAccount.setAccount(111222333);
bankAccount.setBankName("Goldman Sachs");
bankAccount.setSwift("GOLDUS33");
bankAccount.setOwner("Donald Trump");
SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
Session session;
Transaction transaction = null;
try {
session = sessionFactory.getCurrentSession();
transaction = session.beginTransaction();
session.persist(creditCard);
session.persist(bankAccount);
transaction.commit();
} catch (Exception e) {
transaction.rollback();
throw e;
}
Session session1;
Transaction transaction1 = null;
try {
session1 = sessionFactory.getCurrentSession();
transaction1 = session1.beginTransaction();
List billingDetails = session1.createQuery("select bd from BillingDetails bd").list();
for (int i = 0; i < billingDetails.size(); i++) {
System.out.println(billingDetails.get(i));
}
} catch (Exception e) {
transaction1.rollback();
throw e;
}
}
}
The BankAccount and CreditCard classes inherit from the common abstract ancestor of BillingDetails. As can be seen from the diagram, despite the similar functionality, their states differ significantly: for the card, the number and validity are important to us, and for the bank account, the details fields.
The parent class stores only owner information common to all descendants.
In addition, one can take out, for example, the Id field together with the type of generation (in this case, we did without it).
The schema of our database for the first strategy will look like this:
Queries for creating tables:
CREDIT_CARD
create table credit_card
(
id serial not null
constraint bank_account_pkey
primary key,
cc_owner varchar(20) not null,
card_number integer not null,
exp_month varchar(9) not null,
exp_year varchar(4) not null
)
;
BANK_ACCOUNT
create table bank_account
(
id serial not null
primary key,
owner varchar(20),
account integer not null,
bank_name varchar(20) not null,
swift varchar(20) not null
)
;
Polymorphism in this case will be implicit. Each child class we can reflect with the Entity annotation .
IMPORTANT! Superclass properties will be ignored by default. To save them to a table of a specific subclass, you must use the @MappedSuperClass annotation.
Displaying subclasses is not unusual. The only thing you should pay attention to is the @AttributeOverride annotation, perhaps unfamiliar to some.
It is used to rename a column in a subclass table if the names of the ancestor and the descendant table do not match (in our case, so that the “owner” from BillingDetails maps to CC_OWNER in the CREDIT_CARD table).
The main problem with using this strategy is that it will be impossible to use polymorphic associations to the full extent: they are usually presented in the database as foreign key access, and we simply do not have the BILLING_DETAILS table. And since each BillingDetails object in the application will be associated with a specific User object, then each of the descendant tables will need a foreign key that refers to the USERS table.
In addition, polymorphic queries will also be a problem.
Let's try to fulfill the request
SELECT bd FROM BillingDetails bd
To do this (hereinafter) just run the main () method.
In this case, it will be performed as follows:
Hibernate:
select
bankaccoun0_.id as id1_1_,
bankaccoun0_.owner as owner2_1_,
bankaccoun0_.account as account3_1_,
bankaccoun0_.bank_name as bank_nam4_1_,
bankaccoun0_.swift as swift5_1_
from
BANK_ACCOUNT bankaccoun0_
Hibernate:
select
creditcard0_.id as id1_2_,
creditcard0_.owner as owner2_2_,
creditcard0_.card_number as card_num3_2_,
creditcard0_.exp_month as exp_mont4_2_,
creditcard0_.exp_year as exp_year5_2_
from
CREDIT_CARD creditcard0_
In other words, for each particular subclass, Hibernate uses a separate SELECT query.
Another important issue when using this strategy will be the complexity of refactoring. Changing the name of the fields in the superclass will cause the need to change the names in many tables and require manual renaming (most IDE tools do not take into account @AttributeOverride). If your scheme does not have 2 tables, but 50, this is fraught with a lot of time.
This approach can be used only at the top of the class hierarchy, where:
a) Polymorphism is not needed (Hibernate will perform a selection for a particular subclass in one request -> performance will be high)
b) No changes are expected in the superclass.
For an application where requests will reference the BillingDetails parent class, this strategy is not suitable.
Strategy 2
One table for each class with joins (UNION)
The role of the abstract class will again be BillingDetails.
The database schema will also remain unchanged.
The only thing is that the CC_OWNER field in the CREDIT_CARD table will have to be renamed OWNER, since this strategy does not support @AttributeOverride. From the documentation :
"The limitation of this approach is that if a property is mapped on the superclass, the column name must be the same on all subclass tables."
The annotation @Inheritance indicated above the superclass with the indicated TABLE_PER_CLASS strategy will also be new.
IMPORTANT! Within the framework of this strategy, the presence of an identifier in a superclass is a mandatory requirement (in the first example, we did without it).
IMPORTANT! According to the JPA standard, the TABLE_PER_CLASS strategy is optional, therefore, other implementations may not be supported.
Modified Java Code
import javax.persistence.*;
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class BillingDetails {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
private String owner;
public BillingDetails() {
}
public int getId() {
return id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
@Override
public String toString() {
return "BillingDetails{" +
"id=" + id +
", owner='" + owner + '\'' +
'}';
}
}
import javax.persistence.*;
@Entity
@Table(name = "CREDIT_CARD")
public class CreditCard extends BillingDetails {
@Column(name = "card_number")
private int cardNumber;
@Column(name = "exp_month")
private String expMonth;
@Column (name = "exp_year")
private String expYear;
public CreditCard() {
}
public int getCardNumber() {
return cardNumber;
}
public String getExpMonth() {
return expMonth;
}
public String getExpYear() {
return expYear;
}
public void setCardNumber(int cardNumber) {
this.cardNumber = cardNumber;
}
public void setExpMonth(String expMonth) {
this.expMonth = expMonth;
}
public void setExpYear(String expYear) {
this.expYear = expYear;
}
@Override
public String toString() {
return "CreditCard{" +
"cardNumber=" + cardNumber +
", expMonth='" + expMonth + '\'' +
", expYear='" + expYear + '\'' +
'}';
}
}
import javax.persistence.*;
@Entity
@Table(name = "BANK_ACCOUNT")
public class BankAccount extends BillingDetails {
private int account;
@Column(name = "bank_name")
private String bankName;
private String swift;
public BankAccount() {
}
public int getAccount() {
return account;
}
public void setAccount(int account) {
this.account = account;
}
public String getBankName() {
return bankName;
}
public void setBankName(String bankName) {
this.bankName = bankName;
}
public String getSwift() {
return swift;
}
public void setSwift(String swift) {
this.swift = swift;
}
@Override
public String toString() {
return "BankAccount{" +
"account=" + account +
", bankName='" + bankName + '\'' +
", swift='" + swift + '\'' +
'}';
}
}
Our SQL schema still knows nothing about inheritance; there is no relationship between tables.
The main advantage of this strategy can be seen by executing the polymorphic query from the previous example.
SELECT bd FROM BillingDetails bd
This time it will be executed differently:
Hibernate:
select
billingdet0_.id as id1_1_,
billingdet0_.owner as owner2_1_,
billingdet0_.card_number as card_num1_2_,
billingdet0_.exp_month as exp_mont2_2_,
billingdet0_.exp_year as exp_year3_2_,
billingdet0_.account as account1_0_,
billingdet0_.bank_name as bank_nam2_0_,
billingdet0_.swift as swift3_0_,
billingdet0_.clazz_ as clazz_
from
( select
id,
owner,
card_number,
exp_month,
exp_year,
null::int4 as account,
null::varchar as bank_name,
null::varchar as swift,
1 as clazz_
from
CREDIT_CARD
union
all select
id,
owner,
null::int4 as card_number,
null::varchar as exp_month,
null::varchar as exp_year,
account,
bank_name,
swift,
2 as clazz_
from
BANK_ACCOUNT
) billingdet0_
In this case, Hibernate uses FROM to retrieve all instances of BillingDetails from all subclass tables. Tables are joined using UNION, and literals (1 and 2) are added to the intermediate result. Literals are used by Hibernate to instantiate the correct class.
Combining tables requires the same column structure, so NULL was inserted instead of non-existing columns (for example, "null :: varchar as bank_name" in credit_card - there is no bank name in the credit card table).
Another important advantage over the first strategy is the ability to use polymorphic associations. Now it will be possible to display associations between the User and BillingDetails classes without any problems.
Strategy 3
A single table for the entire class hierarchy
The class hierarchy can be completely selected in one table. It will contain columns for all fields of each class in the hierarchy. For each record, a particular subclass will be determined by the value of an additional column with a selector .
Our diagram now looks like this:
Request to create
create table billing_details
(
id serial not null
constraint billing_details_pkey
primary key,
bd_type varchar(2),
owner varchar(20),
card_number integer,
exp_month varchar(9),
exp_year varchar(4),
account integer,
bank_name varchar(20),
swift varchar(20)
)
;
create unique index billing_details_card_number_uindex
on billing_details (card_number)
;
Structure of Java classes:
To create a mapping with a single table, you must use the inheritance strategy SINGLE_TABLE.
The root class will be mapped to table BILLING_DETAILS. To distinguish between types, a selector column will be used. It is not an entity field and was created only for the needs of Hibernate. Its value will be strings - “CC” or “BA”.
IMPORTANT! If you do not explicitly specify the selector column in the superclass, it will get the default name DTYPE and type VARCHAR.
Each hierarchy class can indicate its selector value using the @DiscriminatorValue annotation.
Do not neglect the explicit name of the selector: by default, Hibernate will use the fully qualified class name or entity name (it depends on whether XML-Hibernate files or JPA / annotation xml files are used).
Modified Java Code
import javax.persistence.*;
@Entity
@Table(name = "BILLING_DETAILS")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "BD_TYPE")
public abstract class BillingDetails {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
private String owner;
public BillingDetails() {
}
public int getId() {
return id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
@Override
public String toString() {
return "BillingDetails{" +
"id=" + id +
", owner='" + owner + '\'' +
'}';
}
}
import javax.persistence.*;
@Entity
@DiscriminatorValue("BA")
public class BankAccount extends BillingDetails {
private int account;
@Column(name = "bank_name")
private String bankName;
private String swift;
public BankAccount() {
}
public int getAccount() {
return account;
}
public void setAccount(int account) {
this.account = account;
}
public String getBankName() {
return bankName;
}
public void setBankName(String bankName) {
this.bankName = bankName;
}
public String getSwift() {
return swift;
}
public void setSwift(String swift) {
this.swift = swift;
}
@Override
public String toString() {
return "BankAccount{" +
"account=" + account +
", bankName='" + bankName + '\'' +
", swift='" + swift + '\'' +
'}';
}
}
import javax.persistence.*;
@Entity
@DiscriminatorValue("CC")
public class CreditCard extends BillingDetails {
@Column(name = "card_number")
private int cardNumber;
@Column(name = "exp_month")
private String expMonth;
@Column (name = "exp_year")
private String expYear;
public CreditCard() {
}
public int getCardNumber() {
return cardNumber;
}
public String getExpMonth() {
return expMonth;
}
public String getExpYear() {
return expYear;
}
public void setCardNumber(int cardNumber) {
this.cardNumber = cardNumber;
}
public void setExpMonth(String expMonth) {
this.expMonth = expMonth;
}
public void setExpYear(String expYear) {
this.expYear = expYear;
}
@Override
public String toString() {
return "CreditCard{" +
"cardNumber=" + cardNumber +
", expMonth='" + expMonth + '\'' +
", expYear='" + expYear + '\'' +
'}';
}
}
For verification, we use the familiar request in the main method
SELECT bd FROM BillingDetails bd
In the case of a single table, this query will be executed as follows:
Hibernate:
select
billingdet0_.id as id2_0_,
billingdet0_.owner as owner3_0_,
billingdet0_.card_number as card_num4_0_,
billingdet0_.exp_month as exp_mont5_0_,
billingdet0_.exp_year as exp_year6_0_,
billingdet0_.account as account7_0_,
billingdet0_.bank_name as bank_nam8_0_,
billingdet0_.swift as swift9_0_,
billingdet0_.BD_TYPE as BD_TYPE1_0_
from
BILLING_DETAILS billingdet0_
If the request is executed to a specific subclass, the line “where BD_TYPE =“ CC ”” will be simply added.
Here's what the mapping into a single table will look like:
In the case where the scheme has been inherited and it is impossible to add a selector column to it, the @DiscriminatorFormula annotation comes to the rescue, which must be added to the parent class. It is necessary to pass the expression CASE ... WHEN into it.
import org.hibernate.annotations.DiscriminatorFormula;
import javax.persistence.*;
@Entity
@Table(name = "BILLING_DETAILS")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula("CASE WHEN CARD_NUMBER IS NOT NULL THEN 'CC' ELSE 'BA' END")
public abstract class BillingDetails {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
//.................
}
The main advantage of this strategy is performance. Queries (both polymorphic and non-polymorphic) are very fast and can be easily written manually. It is not necessary to use connections and unions. The evolution of the circuit is also very simple.
However, the problems surrounding this strategy will often outweigh its benefits.
The main one is data integrity. Columns of those properties that are declared in subclasses may contain NULL. As a result, a simple software error can lead to a credit card with no number or expiration date in the database.
Another problem will be a violation of normalization, and in particular of the third normal form. In this light, the benefits of increased productivity already look doubtful. After all, you will have to at least sacrifice the convenience of escort: in the long term, denormalized schemes do not bode well.
Strategy 4
One table for each class using joins (JOIN)
The scheme of our classes will remain unchanged:
But in the database scheme, some changes have occurred
Request to create BILLING_DETAILS
create table billing_details
(
id integer not null
constraint billing_details_pkey
primary key,
owner varchar(20) not null
)
;
For CREDIT_CARD
create table credit_card
(
id integer not null
constraint credit_card_pkey
primary key
constraint credit_card_billing_details_id_fk
references billing_details,
card_number integer not null,
exp_month varchar(255) not null,
exp_year varchar(255) not null
)
;
create unique index credit_card_card_number_uindex
on credit_card (card_number)
;
For BANK_ACCOUNT
create table bank_account
(
id integer not null
constraint bank_account_pkey
primary key
constraint bank_account_billing_details_id_fk
references billing_details,
account integer not null,
bank_name varchar(255) not null,
swift varchar(255) not null
)
;
create unique index bank_account_account_uindex
on bank_account (account)
;
In Java code, you must use the JOINED strategy to create such a mapping.
Modified Java Code
import javax.persistence.*;
@Entity
@Table(name = "BILLING_DETAILS")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class BillingDetails {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
private String owner;
public BillingDetails() {
}
public int getId() {
return id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
@Override
public String toString() {
return "BillingDetails{" +
"id=" + id +
", owner='" + owner + '\'' +
'}';
}
}
import javax.persistence.*;
@Entity
@Table(name = "CREDIT_CARD")
public class CreditCard extends BillingDetails {
@Column(name = "card_number")
private int cardNumber;
@Column(name = "exp_month")
private String expMonth;
@Column (name = "exp_year")
private String expYear;
public CreditCard() {
}
public int getCardNumber() {
return cardNumber;
}
public String getExpMonth() {
return expMonth;
}
public String getExpYear() {
return expYear;
}
public void setCardNumber(int cardNumber) {
this.cardNumber = cardNumber;
}
public void setExpMonth(String expMonth) {
this.expMonth = expMonth;
}
public void setExpYear(String expYear) {
this.expYear = expYear;
}
@Override
public String toString() {
return "CreditCard{" +
"cardNumber=" + cardNumber +
", expMonth='" + expMonth + '\'' +
", expYear='" + expYear + '\'' +
'}';
}
}
import javax.persistence.*;
@Entity
@Table(name = "BANK_ACCOUNT")
public class BankAccount extends BillingDetails {
private int account;
@Column(name = "bank_name")
private String bankName;
private String swift;
public BankAccount() {
}
public int getAccount() {
return account;
}
public void setAccount(int account) {
this.account = account;
}
public String getBankName() {
return bankName;
}
public void setBankName(String bankName) {
this.bankName = bankName;
}
public String getSwift() {
return swift;
}
public void setSwift(String swift) {
this.swift = swift;
}
@Override
public String toString() {
return "BankAccount{" +
"account=" + account +
", bankName='" + bankName + '\'' +
", swift='" + swift + '\'' +
'}';
}
}
Now, when saving, for example, an instance of CreditCard, Hibernate will insert two records. The properties declared in the fields of the BillingDetails superclass will fall into the BILLING_DETAILS table, and the values of the fields of the CreaditCard subclass will be written to the CREDIT_CARD table. These entries will be combined by a common primary key.
Thus, the circuit was brought to normal. The evolution of the circuit and the determination of integrity constraints are also simple.
Foreign keys allow you to represent a polymorphic association with a particular subclass.
By completing the request
SELECT bd FROM BillingDetails bd
, we will see the following picture:
Hibernate:
select
billingdet0_.id as id1_1_,
billingdet0_.owner as owner2_1_,
billingdet0_1_.card_number as card_num1_2_,
billingdet0_1_.exp_month as exp_mont2_2_,
billingdet0_1_.exp_year as exp_year3_2_,
billingdet0_2_.account as account1_0_,
billingdet0_2_.bank_name as bank_nam2_0_,
billingdet0_2_.swift as swift3_0_,
case
when billingdet0_1_.id is not null then 1
when billingdet0_2_.id is not null then 2
when billingdet0_.id is not null then 0
end as clazz_
from
BILLING_DETAILS billingdet0_
left outer join
CREDIT_CARD billingdet0_1_
on billingdet0_.id=billingdet0_1_.id
left outer join
BANK_ACCOUNT billingdet0_2_
on billingdet0_.id=billingdet0_2_.id
BILLING_DETAILS
CREDIT_CARD
BANK_ACCOUNT
The CASE ... WHEN clause allows Hibernate to define a specific subclass for each record. It checks for the presence or absence of rows in the tables of subclasses CREDIR_CARD and BANK_ACCOUNT using literals.
Such a strategy will be very difficult to implement manually. Even implementing reports based on arbitrary queries will be much more difficult.
Performance may also be unacceptable for a particular project, because queries will require joining multiple tables or many sequential read operations.
Mixing inheritance mapping strategies
When working with the strategies TABLE_PER_CLASS, SINGLE_TABLE and JOINED, a significant inconvenience is the fact that it is impossible to switch between them. You will have to adhere to the chosen strategy until the end (or completely change the scheme).
But there are tricks with which you can switch the display strategy for a particular subclass.
For example, displaying the class hierarchy in a single table (strategy 3), you can select a strategy with a separate table and foreign key for a separate subclass (strategy 4).
Script to create BILLING_DETAILS
create table billing_details
(
id integer not null
constraint billing_details_pkey
primary key,
owner varchar(20),
account integer,
bank_name varchar(20),
swift varchar(20)
)
;
For CREDIT_CARD
create table credit_card
(
card_number integer not null,
exp_month varchar(255) not null,
exp_year varchar(255) not null,
id integer not null
constraint credit_card_pkey
primary key
constraint fksf645frtr6h3i4d179ff4ke9h
references billing_details
)
;
Now we can map the CreditCard subclass into a separate table.
To do this, we will need to apply the InheritanceType.SINGLE_TABLE strategy to the BillingDetails superclass, and the @SecondaryTable annotation will help us in working with the CreditCard class.
Modified Java Code
import javax.persistence.*;
@Entity
@Table(name = "BILLING_DETAILS")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "BD_TYPE")
public abstract class BillingDetails {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
private String owner;
public BillingDetails() {
}
public int getId() {
return id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
@Override
public String toString() {
return "BillingDetails{" +
"id=" + id +
", owner='" + owner + '\'' +
'}';
}
}
import javax.persistence.*;
@Entity
public class BankAccount extends BillingDetails {
private int account;
@Column(name = "bank_name")
private String bankName;
private String swift;
public BankAccount() {
}
public int getAccount() {
return account;
}
public void setAccount(int account) {
this.account = account;
}
public String getBankName() {
return bankName;
}
public void setBankName(String bankName) {
this.bankName = bankName;
}
public String getSwift() {
return swift;
}
public void setSwift(String swift) {
this.swift = swift;
}
@Override
public String toString() {
return "BankAccount{" +
"account=" + account +
", bankName='" + bankName + '\'' +
", swift='" + swift + '\'' +
'}';
}
}
import javax.persistence.*;
@Entity
@DiscriminatorValue("CC")
@SecondaryTable(name = "CREDIT_CARD",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "ID"))
public class CreditCard extends BillingDetails {
@Column(table = "CREDIT_CARD",name = "card_number")
private int cardNumber;
@Column(table = "CREDIT_CARD",name = "exp_month")
private String expMonth;
@Column (table = "CREDIT_CARD",name = "exp_year")
private String expYear;
public CreditCard() {
}
public int getCardNumber() {
return cardNumber;
}
public String getExpMonth() {
return expMonth;
}
public String getExpYear() {
return expYear;
}
public void setCardNumber(int cardNumber) {
this.cardNumber = cardNumber;
}
public void setExpMonth(String expMonth) {
this.expMonth = expMonth;
}
public void setExpYear(String expYear) {
this.expYear = expYear;
}
@Override
public String toString() {
return "CreditCard{" +
"cardNumber=" + cardNumber +
", expMonth='" + expMonth + '\'' +
", expYear='" + expYear + '\'' +
'}';
}
}
Using the annotations @SecondaryTable and @Column, we redefine the main table and its columns, indicating to Hibernate where to get the data from.
When you select the SINGLE_TABLE strategy, subclass columns may contain NULL. Using this technique, you can guarantee data integrity for a particular subclass (in our case, CreditCard).
By executing a polymorphic query, Hibernate will perform an outer join to retrieve instances of BillingDetails and all its subclasses.
Let's try:
SELECT bd FROM BillingDetails bd
Result:
Hibernate:
select
billingdet0_.id as id2_0_,
billingdet0_.owner as owner3_0_,
billingdet0_.account as account4_0_,
billingdet0_.bank_name as bank_nam5_0_,
billingdet0_.swift as swift6_0_,
billingdet0_1_.card_number as card_num1_1_,
billingdet0_1_.exp_month as exp_mont2_1_,
billingdet0_1_.exp_year as exp_year3_1_,
billingdet0_.BD_TYPE as BD_TYPE1_0_
from
BILLING_DETAILS billingdet0_
left outer join
CREDIT_CARD billingdet0_1_
on billingdet0_.id=billingdet0_1_.ID
This technique can be applied to other classes of the hierarchy, but for an extensive hierarchy, it does not work too well, since external connection in this case will become a problem. For such a hierarchy, a strategy that will immediately execute a second SQL query instead of an external join is best suited.
Strategy selection
Each of the above strategies and techniques has its advantages and disadvantages. General recommendations for choosing a specific strategy will look like this:
- Strategy number 2 (TABLE_PER_CLASS based on UNION), if polymorphic queries and associations are not required. If you rarely (or don’t execute at all) “select bd from BillingDetails bd” and you don’t have classes that reference BillingDetails, this option would be better (since you will still be able to add optimized polymorphic queries and associations).
- Strategy number 3 (SINGLE_TABLE) should be used:
a) Only for simple tasks. In situations where normalization and NOT NULL constraint are critical, strategy # 4 (JOINED) should be preferred. It makes sense to think about whether it is worthwhile to abandon inheritance altogether and replace it with delegation
b) If polymorphic queries and associations are required, as well as the dynamic definition of a particular class at run time; however, subclasses declare relatively few new fields, and the main difference with the superclass is the behavior.
Well, in addition to this, you have a serious conversation with the DBA.
- Strategy # 4 (JOINED) is suitable in cases where polymorphic queries and associations are required, but subclasses declare relatively many new fields.
It is worth mentioning here: the decision between JOINED and TABLE_PER_CLASS requires evaluating plans for query execution on real data, since the width and depth of the inheritance hierarchy can make the cost of connections (and, as a result, performance) unacceptable.
We should also take into account that inheritance annotations cannot be applied to interfaces.
Thanks for attention!