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.

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!

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:

image

Java code to run the example
pom.xml:

4.0.0com.hiber.jd2050hiberLearn1.0-SNAPSHOTUTF-81.7org.apache.maven.pluginsmaven-compiler-plugin3.1${java.version}postgresqlpostgresql9.0-801.jdbc4org.hibernate.javax.persistencehibernate-jpa-2.1-api1.0.0.Finaljavax.transactionjta1.1org.hibernatehibernate-core5.0.5.Final

hibernate.cfg.xml

jdbc:postgresql://localhost:5432/dobrynin_dborg.postgresql.DriverpostgresfilyaSl9999org.hibernate.dialect.PostgreSQL9Dialectcreate-droptruetruethread

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:
image

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:
image

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:

image

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:
image

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:

image

But in the database scheme, some changes have occurred

image

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

image

CREDIT_CARD

image

BANK_ACCOUNT

image

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).

image

image

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

image

image

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!

Also popular now: