Hibernate Inheritance Mapping

This lesson covers the different strategies for mapping inheritance hierarchies to database tables in Hibernate. We will explore strategies like Table per Class Hierarchy, Table per Subclass, and Table per Concrete Class.


Table Per Class Hierarchy in Hibernate

This document provides an in-depth explanation of the Table per Class Hierarchy strategy for inheritance mapping in Hibernate, including its advantages, disadvantages, and implementation details with examples.

What is the Table per Class Hierarchy Strategy?

The Table per Class Hierarchy strategy maps an entire class hierarchy to a single database table. This single table contains all the columns required to represent all the attributes of all classes in the hierarchy. A discriminator column is used to distinguish between different subclasses. This strategy is simple to implement but can lead to large, sparse tables if the subclasses have significantly different attributes.

In-Depth Explanation

In this strategy, all the properties of all classes in the inheritance hierarchy are mapped to columns in a single table. A special column, called the *discriminator column*, stores a value that indicates which subclass a particular row represents. When a subclass entity is retrieved, Hibernate filters the rows based on the discriminator value to load the correct subclass data.

Advantages:

  • Simple Implementation: Easy to set up and understand.
  • Good Performance for Polymorphic Queries: Efficient when querying for all objects in the hierarchy because all data resides in a single table. No joins are needed.
  • Simple Schema: Only one table to manage.

Disadvantages:

  • Large, Sparse Table: The table can become very wide and sparse, containing many nullable columns that apply to only some subclasses.
  • Wasted Storage Space: Columns that don't apply to a specific subclass will be null, potentially wasting storage space.
  • Difficult to Add New Properties to Subclasses: Adding a new property to a specific subclass requires adding a new column to the shared table, potentially affecting other subclasses.
  • Data Integrity Issues: Constraints can be difficult to enforce at the database level, especially non-nullable constraints that apply to only some subclasses.
  • Performance Impact on Large Hierarchies: On very large hierarchies, the size of the table can negatively impact performance.

Implementation with Hibernate Annotations

The @Inheritance annotation with the strategy = InheritanceType.SINGLE_TABLE is used to specify the Table per Class Hierarchy strategy. The @DiscriminatorColumn annotation specifies the name and type of the discriminator column, and the @DiscriminatorValue annotation specifies the value that identifies each subclass.

Base Class (BillingDetails.java):

 import javax.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "billing_type", discriminatorType = DiscriminatorType.STRING)
public abstract class BillingDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String owner;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }
} 

CreditCard Class (CreditCard.java):

 import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("CC")
public class CreditCard extends BillingDetails {

    private String cardNumber;
    private String expiryMonth;
    private String expiryYear;

    public String getCardNumber() {
        return cardNumber;
    }

    public void setCardNumber(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    public String getExpiryMonth() {
        return expiryMonth;
    }

    public void setExpiryMonth(String expiryMonth) {
        this.expiryMonth = expiryMonth;
    }

    public String getExpiryYear() {
        return expiryYear;
    }

    public void setExpiryYear(String expiryYear) {
        this.expiryYear = expiryYear;
    }
} 

BankAccount Class (BankAccount.java):

 import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("BA")
public class BankAccount extends BillingDetails {

    private String accountNumber;
    private String bankName;
    private String swiftCode;

    public String getAccountNumber() {
        return accountNumber;
    }

    public void setAccountNumber(String accountNumber) {
        this.accountNumber = accountNumber;
    }

    public String getBankName() {
        return bankName;
    }

    public void setBankName(String bankName) {
        this.bankName = bankName;
    }

    public String getSwiftCode() {
        return swiftCode;
    }

    public void setSwiftCode(String swiftCode) {
        this.swiftCode = swiftCode;
    }
} 

Implementation with Hibernate XML Configuration

The <hibernate-mapping> element is the root element. The <class> element maps a class to a table, and the <id> element maps the identifier property. The <discriminator> element specifies the discriminator column and its type. Each subclass is mapped using the <subclass> element, with a discriminator-value attribute specifying the discriminator value for that subclass. The properties for each class are defined inside the corresponding <class> or <subclass> element using the <property> element.

Hibernate Mapping File (billing-details.hbm.xml):

 <hibernate-mapping package="your.package.name"> <class name="BillingDetails" table="BillingDetails"> <id name="id" column="id"> <generator class="identity"/>
        </id> <discriminator column="billing_type" type="string"/>

        <property name="owner" column="owner"/>

        <subclass name="CreditCard" discriminator-value="CC"> <property name="cardNumber" column="cardNumber"/>
            <property name="expiryMonth" column="expiryMonth"/>
            <property name="expiryYear" column="expiryYear"/>
        </subclass> <subclass name="BankAccount" discriminator-value="BA"> <property name="accountNumber" column="accountNumber"/>
            <property name="bankName" column="bankName"/>
            <property name="swiftCode" column="swiftCode"/>
        </subclass> </class> </hibernate-mapping>

Note: Replace your.package.name with the actual package name where your classes are located.

Example Code: Mapping and Querying

This section demonstrates how to persist and query data using the Table per Class Hierarchy strategy with Hibernate.

Java Code (HibernateExample.java):

 import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

import java.util.List;

public class HibernateExample {

    public static void main(String[] args) {
        // Create a Hibernate configuration
        Configuration configuration = new Configuration().configure("hibernate.cfg.xml")
                .addAnnotatedClass(BillingDetails.class)
                .addAnnotatedClass(CreditCard.class)
                .addAnnotatedClass(BankAccount.class);


        // Build the SessionFactory
        SessionFactory sessionFactory = configuration.buildSessionFactory();

        // Open a session
        Session session = sessionFactory.openSession();
        Transaction transaction = null;


        try {
            // Begin a transaction
            transaction = session.beginTransaction();

            // Create and persist CreditCard object
            CreditCard creditCard = new CreditCard();
            creditCard.setOwner("John Doe");
            creditCard.setCardNumber("1234567890123456");
            creditCard.setExpiryMonth("12");
            creditCard.setExpiryYear("2025");
            session.save(creditCard);

            // Create and persist BankAccount object
            BankAccount bankAccount = new BankAccount();
            bankAccount.setOwner("Jane Smith");
            bankAccount.setAccountNumber("9876543210");
            bankAccount.setBankName("Example Bank");
            bankAccount.setSwiftCode("EXMPLXXX");
            session.save(bankAccount);

            // Commit the transaction
            transaction.commit();

            // Query all BillingDetails
            session = sessionFactory.openSession(); // New session for querying avoids potential issues
            List<BillingDetails> billingDetailsList = session.createQuery("from BillingDetails", BillingDetails.class).list();
            System.out.println("All Billing Details:");
            for (BillingDetails billingDetails : billingDetailsList) {
                System.out.println(billingDetails.getClass().getSimpleName() + ": " + billingDetails.getOwner());
            }

            //Query credit card only
            List<CreditCard> creditCards = session.createQuery("from CreditCard", CreditCard.class).list();
            System.out.println("All Credit Cards:");
            for(CreditCard cc : creditCards){
                System.out.println("Credit Card: " + cc.getOwner() + " - " + cc.getCardNumber());
            }


        } catch (Exception e) {
            if (transaction != null) {
                transaction.rollback();
            }
            e.printStackTrace();
        } finally {
            if (session != null) {
                session.close();
            }
            sessionFactory.close();
        }
    }
} 

Hibernate Configuration File (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> <!-- Database connection settings --> <property name="connection.driver_class">org.h2.Driver</property> <property name="connection.url">jdbc:h2:mem:testdb</property> <property name="connection.username">sa</property> <property name="connection.password"></property> <!-- JDBC connection pool (use the built-in) --> <property name="connection.pool_size">1</property> <!-- SQL dialect --> <property name="dialect">org.hibernate.dialect.H2Dialect</property> <!-- Enable Hibernate's automatic session context management --> <property name="current_session_context_class">thread</property> <!-- Disable the second-level cache  --> <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property> <!-- Echo all executed SQL to stdout --> <property name="show_sql">true</property> <!-- Drop and re-create the database schema on startup --> <property name="hbm2ddl.auto">create-drop</property> <!--  Mapping files  --> <!-- <mapping resource="billing-details.hbm.xml"/> -->  <!-- Uncomment if using XML mappings --> </session-factory> </hibernate-configuration>

This example uses an in-memory H2 database for simplicity. You'll need to add the H2 database dependency to your project (e.g., using Maven or Gradle). If you're using XML mapping, uncomment the <mapping resource="billing-details.hbm.xml"/> line in the hibernate.cfg.xml file and remove the .addAnnotatedClass lines from the configuration in the `HibernateExample.java` file.