Наследование в Hibernate: выбор стратегии

в 13:31, , рубрики: hibernate, java, Программирование

Наследование является одним из основных принципов ООП. В то же время, значительное количество корпоративных приложений имеют в своей основе реляционные базы данных

Главное противоречие между объектно-ориентированной и реляционной моделями заключается в том, объектная модель поддерживает два вида отношений («is a» — “является”, и «has a» — “имеет”), а модели, основанные на SQL, поддерживают только отношения «has a».

Иными словами, SQL не понимает наследование типов и не поддерживает его.

Поэтому на этапе построения сущностей и схемы БД одной из главных задач разработчика будет выбор оптимальной стратегии представления иерархии наследования.

Всего таких стратегий 4:

1) Использовать одну таблицу для каждого класса и полиморфное поведение по умолчанию.

2) Одна таблица для каждого конкретного класса, с полным исключением полиморфизма и отношений наследования из схемы SQL (для полиморфного поведения во время выполнения будут использоваться UNION-запросы)

3) Единая таблица для всей иерархии классов. Возможна только за счет денормализации схемы SQL. Определять суперкласс и подклассы будет возможно посредством различия строк.

4) Одна таблица для каждого подкласса, где отношение “is a” представлено в виде «has a», т.е. – связь по внешнему ключу с использованием JOIN.

Можно выделить 3 главных фактора, на которые повлияет выбранная вами стратегия:

1) Производительность (мы используем “hibernate_show_sql”, чтобы увидеть и оценить все выполняемые к БД запросы)

2) Нормализация схемы и гарантия целостности данных (не каждая стратегия гарантирует выполнение ограничения NOT NULL)

3) Возможность эволюции вашей схемы

Под катом каждая из этих стратегий будет рассмотрена подробно, с указанием преимуществ и недостатков, а также будут даны рекомендации по выбору стратегии в конкретных случаях.

Пара слов от автора и инструкция по работе с примерами
Данная статья является выжимкой из книги «Java Persistance with Hibernate». Ее авторы — основатель проекта Hibernate Гэвин Кинг (Gavin King) и член команды разработчиков Hibernate Кристиан Баэур (Christian Bauer).
Летом 2017 она была переведена и издана на русском языке.

Я постарался упростить изложение материала, а также работу с примерами. Испытывая сильную нелюбовь к примерам, с которыми для запуска нужно возиться час, я стремился сделать работу с ними в этой статье максимально удобной:
— Весь Java-код вы можете просто скопировать в свою IDE. Все изменения Java-кода при переходе от одной стратегии к другой указаны в спойлерах, поэтому при переходе к новой стратегии старый код класса можно просто удалить и скопировать новый. Классы Main и HibernateUtil останутся без изменений, и будут работать при рассмотрении всех примеров.

— В спойлерах к каждой стратегии вы также найдете скрипты для создания всех таблиц БД. Поэтому после того, как вы разобрали очередную стратегию, можно просто дропнуть все таблицы — в следующем разделе вы найдете актуальные скрипты для создания новых.

Код написан с использованием Java 1.7, Hibernate5 и PostgreSQL9

Приятного прочтения!

Стратегия 1

Одна таблица для каждого класса

Ситуация:

Мы решили затмить славу eBay и создаем для этой цели свое приложение интернет-аукциона. Каждый User может делать ставки, и в том случае если его ставка оказалась самой крупной – совершить оплату онлайн.

Собственно, процесс оплаты мы и будем рассматривать в качестве модели данных.
User может совершить оплату двумя способами: при помощи банковской карты, или посредством реквизитов банковского счета.

Диаграмма классов представлена ниже:

image

Java-код для запуска примера

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.hiber.jd2050</groupId>
    <artifactId>hiberLearn</artifactId>
    <version>1.0-SNAPSHOT</version>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>


        <!-- PostgreSQL -->
        <dependency>
            <groupId>postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>9.0-801.jdbc4</version>
        </dependency>


        <!-- Hibernate-JPA-2.1-API -->
        <dependency>
            <groupId>org.hibernate.javax.persistence</groupId>
            <artifactId>hibernate-jpa-2.1-api</artifactId>
            <version>1.0.0.Final</version>
        </dependency>


        <dependency>
            <groupId>javax.transaction</groupId>
            <artifactId>jta</artifactId>
            <version>1.1</version>
        </dependency>

        <!-- Hibernate-core -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.0.5.Final</version>
        </dependency>

    </dependencies>

</project>

hibernate.cfg.xml

<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>

        <property name="connection.url">jdbc:postgresql://localhost:5432/dobrynin_db</property> <!-- BD Mane -->
        <property name="connection.driver_class">org.postgresql.Driver</property> <!-- DB Driver -->
        <property name="connection.username">postgres</property> <!-- DB User -->
        <property name="connection.password">filyaSl9999</property> <!-- DB Password -->

        <property name="dialect">org.hibernate.dialect.PostgreSQL9Dialect</property> <!-- DB Dialect -->
        <property name="hbm2ddl.auto">create-drop</property> <!-- create / create-drop / update -->

        <property name="show_sql">true</property> <!-- Show SQL in console -->
        <property name="format_sql">true</property> <!-- Show SQL formatted -->
        <property name="hibernate.current_session_context_class">thread</property>
        <mapping class="CreditCard"/>
        <mapping class="BankAccount"/>
        <mapping class="BillingDetails"/>


    </session-factory>
</hibernate-configuration>

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 с методом main():

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;
        }

    }
}

Классы BankAccount и CreditCard наследуются от общего абстрактного предка BillingDetails. Как видно из схемы, несмотря на похожий функционал, их состояния существенно отличаются: для карты нам важны номер и срок действия, а для банковского счета – поля реквизитов.

Родительский класс хранит только общую для всех потомков информацию о владельце.
Кроме того, туда можно вынести, например, поле Id вместе с типом генерации (в данном случае мы обошлись без этого).

Схема нашей БД для первой стратегии будет выглядеть так:
image

Запросы для создания таблиц:

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

Полиморфизм в данном случае будет неявным. Каждый класс-потомок мы можем отразить с помощью аннотации Entity.

ВАЖНО! Свойства суперкласса по умолчанию будут проигнорированы. Чтобы сохранить их в таблицу конкретного подкласса, необходимо использовать аннотацию @MappedSuperClass.

Отображение подклассов не содержит ничего необычного. Единственное, на что следует обратить внимание – возможно, незнакомая для некоторых аннотация @AttributeOverride.
Она используется для переименования столбца в таблице подкласса, в том случае если названия у предка и таблицы потомка не совпадают (в нашем случае – чтобы «owner» из BillingDetails маппился на CC_OWNER в таблице CREDIT_CARD).

Главная проблема при использовании данной стратегии заключается в том, что использовать полиморфные ассоциации в полной мере будет невозможно: обычно они представлены в БД в виде доступа по внешнему ключу, а у нас попросту нет таблицы BILLING_DETAILS. А поскольку каждый объект BillingDetails будет в приложении связан с конкретным объектом User, то каждой из таблиц-«потомков» нужен будет внешний ключ, ссылающийся на таблицу USERS.

Кроме того, проблемой также будут и полиморфные запросы.

Попробуем выполнить запрос

SELECT bd FROM BillingDetails bd

Для этого (здесь и далее) просто запустите метод main().

В данном случае он будет выполнен следующим образом:

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_

Иными словами, для каждого конкретного подкласса Hibernate использует отдельный SELECT-запрос.

Другой важной проблемой при использовании данной стратегии будет сложность рефакторинга. Изменение названия полей в суперклассе вызовет необходимость изменения названий во многих таблицах и потребует ручного переименования (инструменты большинства IDE не учитывают @AttributeOverride). В случае, если в вашей схеме не 2 таблицы, а 50, это чревато большими временными затратами.

Этот подход возможно использовать только для верхушки иерархии классов, где:

а) Полиморфизм не нужен (выборку для конкретного подкласса Hibernate будет выполнять в один запрос -> производительность будет высокой)

б) Изменения в суперклассе не предвидятся.

Для приложения, где запросы будут ссылаться на родительский класс BillingDetails эта стратегия не подойдет.

Стратегия 2

Одна таблица для каждого класса с объединениями (UNION)

В роли абстрактного класса вновь выступит BillingDetails.
Схема БД также останется без почти без изменений.

Единственный момент – поле CC_OWNER в таблице CREDIT_CARD придется переименовать в OWNER, поскольку данная стратегия не поддерживает @AttributeOverride. Из документации:
«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».

Новой также будет указанная над суперклассом аннотация @Inheritance с указанием выбранной стратегии TABLE_PER_CLASS.

ВАЖНО! В рамках данной стратегии наличие идентификатора в суперклассе является обязательным требованием (в первом примере мы обошлись без него).

ВАЖНО! Согласно стандарту JPA стратегия TABLE_PER_CLASS не является обязательной, поэтому другими реализациями может не поддерживаться.

Измененный Java-код

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 + ''' +
                '}';
    }
}

Наша схема SQL по-прежнему ничего не знает о наследовании; между таблицами нет никаких отношений.

Главное преимущество данной стратегии можно увидеть, выполнив полиморфный запрос из предыдущего примера.

SELECT bd FROM BillingDetails bd

На сей раз он будет выполнен по-другому:

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_

В данном случае Hibernate использует FROM, чтобы извлечь все экземпляры BillingDetails из всех таблиц подклассов. Таблицы объединяются с помощью UNION, а в промежуточный результат добавляются литералы (1 и 2). Литералы используются Hibernate для создания экземпляра правильного класса.

Объединение таблиц требует одинаковой структуры столбцов, поэтому вместо несуществующих столбцов были вставлены NULL (например, «null::varchar as bank_name» в credit_card – в таблице кредиток нет названия банка).

Другим важный преимуществом по сравнению с первой стратегией будет возможность использовать полиморфные ассоциации. Теперь можно будет без проблем отобразить ассоциации между классами User и BillingDetails.

Стратегия 3

Единая таблица для всей иерархии классов

Иерархию классов можно целиком отобрать в одну таблицу. Она будет содержать столбцы для всех полей каждого класса иерархии. Для каждой записи конкретный подкласс будет определяться значением дополнительного столбца с селектором.

Наша схема теперь выглядит вот так:
image

Запрос для создания


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

Структура Java-классов:

image

Для создания отображения с одной таблицей необходимо использовать стратегию наследования SINGLE_TABLE.
Корневой класс будет отображен в таблицу BILLING_DETAILS. Для различения типов будет использован столбец селектора. Он не является полем сущности и создан только для нужд Hibernate. Его значением будут строки – “CC” или “BA”.
ВАЖНО! Если не указать столбец селектора в суперклассе явно – он получит название по умолчанию DTYPE и тип VARCHAR.

Каждый класс иерархии может указать свое значение селектора с помощью аннотации @DiscriminatorValue.
Не стоит пренебрегать явным указанием имени селектора: по умолчанию Hibernate будет использовать полное имя класса или имя сущности (зависит от того, используются ли файлы XML-Hibernate или xml-файлы JPA/аннотации).

Измененный Java-код

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 + ''' +
                '}';
    }
}

Для проверки используем в методе main уже привычный запрос

SELECT bd FROM BillingDetails bd

В случае с единой таблицей этот запрос будет выполнен так:

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_

Если же запрос выполняется к конкретному подклассу – будет просто добавлена строка «where BD_TYPE = “CC”».

Вот как будет выглядеть отображение в единую таблицу:
image

В случае, когда схема была унаследована, и добавить в нее столбец селектора невозможно, на помощь приходит аннотация @DiscriminatorFormula, которую необходимо добавить к родительскому классу. В нее необходимо передать выражение CASE...WHEN.

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;

    //.................
}

Главным плюсом данной стратегии является производительность. Запросы (как полиморфные, так и неполиморфные) выполняются очень быстро и могут быть легко написаны вручную. Не приходится использовать соединения и объединения. Эволюция схемы также производится очень просто.

Однако, проблемы, сопровождающие эту стратегию, часто будут перевешивать ее преимущества.

Главной из них является целостность данных. Столбцы тех свойств, которые объявлены в подклассах, могут содержать NULL. В результате простая программная ошибка может привести к тому, что в базе данных окажется кредитная карта без номера или без срока действия.

Другой проблемой будет нарушение нормализации, а конкретно – третьей нормальной формы. В этом свете выгоды от повышенной производительности уже выглядят сомнительно. Ведь придется, как минимум, пожертвовать удобством сопровождения: в долгосрочной перспективе денормализованные схемы не сулят ничего хорошего.

Стратегия 4

Одна таблица для каждого класса с использованием соединений (JOIN)

Схема наших классов останется неизменной:

image

А вот в схеме БД произошли некоторые изменения

image

Запрос для создания BILLING_DETAILS


create table billing_details
(
	id integer not null
		constraint billing_details_pkey
			primary key,
	owner varchar(20) not null
)
;

Для 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)
;

Для 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)
;

В Java-коде для создания такого отображения необходимо использовать стратегию JOINED.

Измененный Java-код

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 + ''' +
                '}';
    }
}

Теперь при сохранении, например, экземпляра CreditCard Hibernate вставит две записи. В таблицу BILLING_DETAILS попадут свойства, объявленные в полях суперкласса BillingDetails, а значения полей подкласса CreaditCard будут записаны в таблицу CREDIT_CARD. Эти записи будут объединены общим первичным ключом.

Таким образом, схема была приведена в нормальное состояние. Эволюция схемы и определение ограничений целостности также осуществляются просто.
Внешние ключи позволяют представить полиморфную ассоциацию с конкретным подклассом.

Выполнив запрос

SELECT bd FROM BillingDetails bd

, мы увидим следующую картину:


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

Предложение CASE…WHEN позволяет Hibernate определить конкретный подкласс для каждой записи. В нем проверяется наличие либо отсутствие строк в таблицах подклассов CREDIR_CARD и BANK_ACCOUNT с помощью литералов.

Подобную стратегию будет весьма непросто реализовать вручную. Даже реализовать отчеты на основе произвольных запросов будет значительно сложнее.
Производительность также может оказаться неприемлемой для конкретного проекта, поскольку запросы потребуют соединения нескольких таблиц или многих последовательных операций чтения.

Смешение стратегий отображения наследования

При работе со стратегиями TABLE_PER_CLASS, SINGLE_TABLE и JOINED значительным неудобством является тот факт, что между ними невозможно переключаться. Выбранной стратегии придется придерживаться до конца (либо полностью менять схему).
Но есть приемы, с помощью которых можно переключить стратегию отображения для конкретного подкласса.

Например, отобразив иерархию классов в единственную таблицу (стратегия 3), можно выбрать для отдельного подкласса стратегию с отдельной таблицей и внешним ключом (стратегия 4).

image

image

Скрипт для создания 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)
)
;

Для 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
)
;

Теперь мы можем отобразить подкласс CreditCard в отдельную таблицу.
Для этого нам нужно будет применить стратегию InheritanceType.SINGLE_TABLE к суперклассу BillingDetails, а в работе с классом CreditCard нам поможет аннотация @SecondaryTable.

Измененный Java-код

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 + ''' +
                '}';
    }
}

При помощи аннотаций @SecondaryTable и @Column мы переопределяем основную таблицу и ее столбцы, указывая Hibernate, откуда необходимо брать данные.

При выборе стратегии SINGLE_TABLE столбцы подклассов могут содержать NULL. Используя же данный прием, вы можете гарантировать целостность данных для конкретного подкласса (в нашем случае — CreditCard).
Исполняя полиморфный запрос, Hibernate выполнит внешнее соединение для извлечения экземпляров BillingDetails и всех его подклассов.

Давайте попробуем:

SELECT bd FROM BillingDetails bd

Результат:


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

Этот прием можно применить и к остальным классам иерархии, но для обширной иерархии он подойдет не слишком хорошо, поскольку внешнее соединение в таком случае станет проблемой. Для такой иерархии лучше подойдет стратегия, которая немедленно выполнит второй SQL-запрос вместо внешнего соединения.

Выбор стратегии

Каждая из перечисленных выше стратегий и приемов имеет свои преимущества и недостатки. Общие рекомендации по выбору конкретной стратегии будут выглядеть так:

— Стратегию №2 (TABLE_PER_CLASS на основе UNION), если полиморфные запросы и ассоциации не требуются. Если вы редко выполняете (или не выполняете вообще) «select bd from BillingDetails bd», и у вас нет классов, ссылающихся на BillingDetails, этот вариант будет лучшим (поскольку возможность добавления оптимизированных полиморфных запросов и ассоциаций сохранится).

— Стратегию №3 (SINGLE_TABLE) стоит использовать:

а) Только для простых задач. В ситуациях, когда нормализация и ограничение NOT NULL являются критическими – следует отдать предпочтение стратегии №4 (JOINED). Имеет смысл задуматься, не стоит ли в данном случае вообще отказаться от наследования и заменить его делегированием
б) Если требуются полиморфные запросы и ассоциации, а также динамическое определение конкретного класса во время выполнения; при этом подклассы объявляют относительно мало новых полей и основная разница с суперклассом заключается в поведении.
Ну и вдобавок к этому, Вам предстоит серьезный разговор с администратором БД.

— Стратегия №4 (JOINED) подойдет в случаях, когда требуются полиморфные запросы и ассоциации, но подклассы объявляют относительно много новых полей.

Здесь стоит оговориться: решение между JOINED и TABLE_PER_CLASS требует оценки планов выполнения запросов на реальных данных, поскольку ширина и глубина иерархии наследования могут сделать стоимость соединений (и, как следствие, производительность) неприемлемыми.

Отдельно стоит принять во внимание, что аннотации наследования невозможно применить к интерфейсам.

Спасибо за внимание!

Автор: jd2050

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js