Data Modeling: Entity Inheritance

Entity inheritance is a key approach for structuring domain models with shared attributes and specialized behavior. In the Petclinic application, for example, pets share common fields like name and birthdate, but specific types such as cats or birds have unique properties. Similarly, visits to the clinic can vary in type, requiring distinct data fields.

This guide explores how we can model those specialities using JPA inheritance strategies. Practical examples from the Petclinic domain model will illustrate how to choose the right strategy for different scenarios.

Requirements

If you want to implement this guide step by step, you will need the following:

  1. Setup Jmix Studio

  2. Download the sample project. You can download the completed sample project, which includes all the examples used in this guide. This allows you to explore the finished implementation and experiment with the functionality right away.

Alternatively, you can start with the base Jmix Petclinic project and follow the step-by-step instructions in this guide to implement the features yourself.

What We are Going to Build

This guide enhances the Jmix Petclinic example by implementing various JPA inheritance strategies:

  • Mapped Superclass (NamedEntity): We will look into the base class NamedEntity that centralizes common fields like id, name, and auditing attributes. Entities such as Owner, PetType, and Specialty inherit these fields.

  • Single Table Inheritance (Visit Hierarchy): The Visit base class encompasses subclasses like RegularCheckup, EmergencyVisit, and FollowUp. All visits are stored in a single table, enabling efficient polymorphic queries and simplifying associations.

  • Joined Table Inheritance (Pet Hierarchy): The Pet base class includes subclasses like Cat and Bird. Subclass-specific attributes are stored in their own tables, while polymorphic queries allow listing all pets.

These scenarios highlight the strengths and trade-offs of each inheritance strategy within the Petclinic domain.

Introduction

Business applications often deal with complex domains that require clear and maintainable representations of shared and specialized data. Entity inheritance allows us to model things that are very similar, but different in certain ways. It provides a way to define common attributes and behavior in a base class while letting subclasses add their own specifics. This not only avoids redundancy but also aligns closely with how you as a developer think about your domain.

In the Petclinic application, we could use entity inheritance to structure the domain model based on real use-cases like:

  • Pets: All pets share attributes such as name and birthDate, but specific types, such as cats and birds, introduce unique fields like litterBoxTrained or wingspan. Inheritance allows these specialized attributes to coexist with common ones.

  • Visits: Different types of visits, such as RegularCheckup or EmergencyVisit, require additional fields specific to their type. At the same time, all visits share common properties like visitStart and treatmentStatus, which can be handled in the base class.

Impedance Mismatch

Object-oriented programming languages like Java naturally support inheritance, allowing you to create hierarchies with shared attributes and specialized behavior. Polymorphism and method overriding make these hierarchies intuitive to design and maintain. In contrast, relational databases are built around flat, tabular structures, where data is stored in rows and columns without inherent support for hierarchical relationships. This fundamental difference is part of the broader "impedance mismatch" between object models and relational databases.

In addition to challenges with inheritance, the impedance mismatch includes difficulties in handling associations, identity, and data types. For example, object references in Java must be mapped to foreign keys in a database, object identity differs from database key constraints, and some data types, like enums or complex objects, require special handling to be stored relationally.

Inheritance is one of the significant aspects of this mismatch. To resolve it, mapping frameworks like JPA use a combination of techniques to bridge the gap. One common approach is to utilize multiple tables to represent an inheritance hierarchy. For example, a base entity might be stored in a separate table, while subclasses have their own tables containing specific attributes. When retrieving data, an SQL JOIN statement is used to combine data from the base and subclass data into a single result set that can be mapped back to the object model.

Discriminator Column

When working with inheritance in relational databases, it becomes crucial to store the type information for each entity instance. This type information ensures that when data is retrieved from the database, it can be accurately mapped back into the appropriate subclass in the object model. To achieve this, a discriminator column is often used. This column stores a value that identifies the specific type of each row in the database.

The discriminator column is a key mechanism to handle polymorphism in relational databases, as it enables mapping frameworks to differentiate between subclasses.

Later in this guide, we will explore how JPA uses discriminator columns as part of its inheritance strategies.

Polymorphic Queries

One solution JPA provides to the impedance mismatch of inheritance is polymorphic queries. This feature allows you to query a base class and transparently retrieve instances of all its subclasses.

SELECT e FROM petclinic_Visit e

This query can fetch both RegularCheckup and EmergencyVisit entities without needing separate queries for each subclass.

This feature simplifies working with inheritance hierarchies. The "heavy lifting" required to express this on the database level—such as JOIN or UNION operations depending on the inheritance strategy—is hidden by JPA. In this guide, we will explore how these strategies impact polymorphic queries and demonstrate their usage in the Petclinic application.

Mapped Superclass

The @MappedSuperclass is the first option to define shared attributes and behavior in a base class without creating a corresponding database table for the superclass. This approach helps for scenarios where entities share common fields, but there is no need for polymorphic querying or explicit relationships between those entities in the database. Strictly speaking it is not an official "inheritance strategy", but still it is quite often used to introduce a hierarchy in the Java domain model.

Data Model

In the Petclinic application, the NamedEntity class illustrates this concept. It defines common system attributes such as id, version, auditing fields (createdBy, createdDate, etc.), and a name field. Subclasses of NamedEntity inherit these attributes, which keeps your entities source code cleaner.

NamedEntity.java
import io.jmix.core.annotation.DeletedBy;
import io.jmix.core.annotation.DeletedDate;
import io.jmix.core.entity.annotation.JmixGeneratedValue;
import io.jmix.core.metamodel.annotation.InstanceName;
import io.jmix.core.metamodel.annotation.JmixEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Version;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;

import java.time.OffsetDateTime;
import java.util.UUID;

@JmixEntity(name = "petclinic_NamedEntity")
@MappedSuperclass
public class NamedEntity {

    @JmixGeneratedValue
    @Column(name = "ID", nullable = false)
    @Id
    private UUID id;

    @InstanceName
    @Column(name = "NAME")
    private String name;

    @Column(name = "VERSION", nullable = false)
    @Version
    private Integer version;

    @CreatedBy
    @Column(name = "CREATED_BY")
    private String createdBy;

    @CreatedDate
    @Column(name = "CREATED_DATE")
    private OffsetDateTime createdDate;

    @LastModifiedBy
    @Column(name = "LAST_MODIFIED_BY")
    private String lastModifiedBy;

    @LastModifiedDate
    @Column(name = "LAST_MODIFIED_DATE")
    private OffsetDateTime lastModifiedDate;

    @DeletedBy
    @Column(name = "DELETED_BY")
    private String deletedBy;

    @DeletedDate
    @Column(name = "DELETED_DATE")
    private OffsetDateTime deletedDate;

}

The NamedEntity superclass is inherited by several entities, including Pet, PetType, and Specialty. As you can see in the source code, the NamedEntity does not have a @Table annotation. This means there is no corresponding table for the NamedEntity itself in the database. Instead, only its subclasses, such as Pet and PetType, define their own database tables.

PetType.java
package io.jmix.petclinic.entity.pet;

import io.jmix.core.metamodel.annotation.JmixEntity;
import io.jmix.petclinic.entity.NamedEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

@JmixEntity
@Table(name = "PETCLINIC_PET_TYPE")
@Entity(name = "petclinic_PetType")
public class PetType extends NamedEntity {
    @Column(name = "COLOR")
    private String color;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}

In addition to inheriting shared fields, the NamedEntity class can also centralize shared Java behavior. For instance, the @InstanceName annotation on the name field defines a consistent display name across all subclasses, such as Pet and Specialty, in the Jmix UI. This eliminates redundant annotations and ensures uniform behavior throughout the domain model.

Database Schema

Each subclass has its own dedicated database table, and the shared fields from NamedEntity are incorporated into these tables. Here is an image of the resulting entity-relationship diagram:

mapped superclass

Because NamedEntity does not have a corresponding table in the database, it cannot be queried directly or by using a polymorphic query. For instance, the following query is not valid:

Invalid query (not supported for @MappedSuperclass)
SELECT n
FROM petclinic_NamedEntity n
WHERE n.name = :name

Instead, queries have to be performed on the specific subclasses. For example:

Query by the name field in Specialty
SELECT s
FROM petclinic_Specialty s
WHERE s.name = :name

These queries operate on the subclass tables but utilize the shared fields defined in NamedEntity. Since the attributes are inherited, they are treated as if they are native to the subclasses in both JPQL queries and in the resulting SQL.

Single Table Inheritance

Single Table Inheritance is a strategy where all entities in an inheritance hierarchy are stored in a single database table. When a subclass has a new attribute, it is added as an additional column in the table. This way, the table holds a superset of the attributes of all subclasses as well as the attributes of the superclass itself.

As we discussed earlier in the Discriminator Column section, JPA provides a mechanism called the discriminator column to determine the specific subclass for each row in the table. In the case of Single Table Inheritance, this mechanism comes into play. The DTYPE column stores a value that identifies the subclass. When JPA reads a row from the VISIT table, it inspects the value in the DTYPE column to instantiate the correct Java object (e.g., RegularCheckup or EmergencyVisit) and populate its attributes with the corresponding column values.

This approach offers simplicity in querying and efficient read operations, as all data resides in one table. However, it comes with the trade-off of potentially large tables with many nullable columns for subclass-specific fields.

Required Fields on Subclasses

One important limitation of Single Table Inheritance is that it does not allow defining NOT NULL constraints for fields specific to subclasses. Since all entities in the hierarchy share the same table, subclass-specific fields must remain nullable to accommodate rows representing other entity types.

This limitation means that enforcing data integrity on subclass-specific fields at the database level is not possible. However, you can still ensure data integrity at the application level using Bean Validation in Jmix. You can use @NotNull on fields in the domain model to validate data before it is persisted to the database.

Let’s look at our example use-case for Single Table Inheritance: modeling visits. Visits can have different types like RegularCheckup and EmergencyVisit. And that’s what we’re going to explore next.

Data Model

Let’s look at the JPA entity definition in more detail. The base class contains attributes shared by all entities, while subclass-specific attributes are added as additional columns. Below is the Visit base class and its two subclasses, RegularCheckup and EmergencyVisit.

Visit.java
package io.jmix.petclinic.entity.visit;

import jakarta.annotation.PostConstruct;
import jakarta.persistence.Table;
import jakarta.persistence.Entity;
import jakarta.persistence.Inheritance;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.DiscriminatorType;
import jakarta.persistence.InheritanceType;


@JmixEntity
@Table(name = "PETCLINIC_VISIT", indexes = {
        @Index(name = "IDX_PETCLINIC_VISIT_ASSIGNED_NURSE", columnList = "ASSIGNED_NURSE_ID"),
        @Index(name = "IDX_PETCLINIC_VISIT_PET", columnList = "PET_ID")
}) (1)
@Entity(name = "petclinic_Visit")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)  (2)
@DiscriminatorColumn(name = "DTYPE", discriminatorType = DiscriminatorType.STRING)  (3)
@DiscriminatorValue("VISIT")
public class Visit {

    @JmixGeneratedValue
    @Column(name = "ID", nullable = false)
    @Id
    private UUID id;

    @JoinColumn(name = "PET_ID", nullable = false)
    @NotNull
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Pet pet;

    @Column(name = "TYPE_", nullable = false)
    @NotNull
    private String type;

    @Column(name = "VISIT_START")
    private LocalDateTime visitStart;

    @Column(name = "VISIT_END")
    private LocalDateTime visitEnd;

    @JoinColumn(name = "ASSIGNED_NURSE_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private User assignedNurse;

    @InstanceName
    @Column(name = "DESCRIPTION", length = 4000)
    private String description;

    @Column(name = "TREATMENT_STATUS")
    private String treatmentStatus;

}
1 The existence of the @Table annotation indicates that the Visit entity has a dedicated table (compared to NamedEntity). It defines the database table PETCLINIC_VISIT.
2 The @Inheritance annotation configures the SINGLE_TABLE strategy, where all entities in the hierarchy are stored in a single table. Subclass attributes are added as additional columns and thus don’t need to configure a @Table annotation on their own.
3 The @DiscriminatorColumn annotation establishes the DTYPE column, which stores the type of each subclass. This ensures that JPA can map database rows to the correct Java objects based on their type.
RegularCheckup.java
package io.jmix.petclinic.entity.visit;

import io.jmix.core.metamodel.annotation.JmixEntity;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;

import java.time.LocalDateTime;


@JmixEntity
@Entity(name = "petclinic_RegularCheckup")
@DiscriminatorValue("REGULAR_CHECKUP")
public class RegularCheckup extends Visit {


    @Column(name = "NEXT_APPOINTMENT_DATE")
    private LocalDateTime nextAppointmentDate;

    public LocalDateTime getNextAppointmentDate() {
        return nextAppointmentDate;
    }

    public void setNextAppointmentDate(LocalDateTime nextAppointmentDate) {
        this.nextAppointmentDate = nextAppointmentDate;
    }

    @PostConstruct
    public void initVisitType() {
        setType(VisitType.REGULAR_CHECKUP);
    }
}
EmergencyVisit.java
package io.jmix.petclinic.entity.visit;

import io.jmix.core.metamodel.annotation.JmixEntity;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;

@JmixEntity
@Entity(name = "petclinic_EmergencyVisit")
@DiscriminatorValue("EMERGENCY_VISIT")
public class EmergencyVisit extends Visit {
    @Column(name = "EMERGENCY_TYPE")
    private String emergencyType;

    @Column(name = "LIFE_THREATENING")
    private Boolean lifeThreatening;

    public Boolean getLifeThreatening() {
        return lifeThreatening;
    }

    public void setLifeThreatening(Boolean lifeThreatening) {
        this.lifeThreatening = lifeThreatening;
    }

    public EmergencyType getEmergencyType() {
        return emergencyType == null ? null : EmergencyType.fromId(emergencyType);
    }

    public void setEmergencyType(EmergencyType emergencyType) {
        this.emergencyType = emergencyType == null ? null : emergencyType.getId();
    }

    @PostConstruct
    public void initVisitType() {
        setType(VisitType.EMERGENCY_VISIT);
    }
}

The subclasses mainly extend Visit indicating that they inherit all attributes and are stored in the VISIT table as specified for the Visit entity. Additionally, the subclasses define the value of the discriminator column for this specific subclass via the @DiscriminatorValue annotation: REGULAR_CHECKUP or EMERGENCY_VISIT.

Database Schema

Let’s look at the resulting entity-relationship diagram of the database:

Entity Relationship Diagram for Single Table Inheritance
Figure 1. Single Table Inheritance ER Diagram

The Visit table consolidates all entities in the inheritance hierarchy. Common fields like id, pet_id, and visit_start are shared across all visit types. The dtype column identifies the specific subclass, while subclass-specific attributes like emergency_type and next_appointment_date are simply stored as additional columns in the same table.

UI Representation

Now, let’s look into how Single Table Inheritance works in the user interface. With Jmix, we can create standard views to handle entities and their subclasses. In this section, we will explore how different visit types (RegularCheckup, EmergencyVisit, and FollowUpVisit) can be managed, from creating new visits to displaying them in polymorphic lists.

Determining the Visit Type Detail View

To create a new visit, we implemented a dialog specifically for this example, allowing users to select the appropriate subclass. The dialog presents options for visit types such as RegularCheckup, EmergencyVisit, and FollowUpVisit. Once a type is selected, the corresponding detail view for the chosen subclass is opened, displaying fields relevant to that type.

Type Selection Dialog for Visits
Figure 2. Type Selection Dialog for Visits

Here is how this is implemented in the controller when creating a new visit:

VisitListView.java
@Route(value = "visits", layout = MainView.class)
@ViewController("petclinic_Visit.list")
@ViewDescriptor("visit-list-view.xml")
@DialogMode(width = "64em")
public class VisitListView extends StandardListView<Visit> {

    @Subscribe("visitsDataGrid.create")
    public void onVisitsDataGridCreate(final ActionPerformedEvent event) {
        dialogs.createInputDialog(this)
                .withHeader(messageBundle.getMessage("createVisitsDialog.header"))
                .withParameter(
                        InputParameter.parameter("type")
                                .withField(() -> {
                                    JmixSelect<Class<? extends Visit>> select = createSelectComponent();

                                    Map<Class<? extends Visit>, String> visitTypes = Map.of( (1)
                                        EmergencyVisit.class, entityCaption(EmergencyVisit.class),
                                        RegularCheckup.class, entityCaption(RegularCheckup.class),
                                        FollowUpVisit.class, entityCaption(FollowUpVisit.class)
                                    );

                                    ComponentUtils.setItemsMap(select, visitTypes);
                                    return select;
                                })
                                .withLabel(messageBundle.getMessage("createVisitsDialog.selectLabel"))
                )
                .withActions(DialogActions.OK_CANCEL)
                .withCloseListener(closeEvent -> {

                    if (closeEvent.closedWith(DialogOutcome.OK)) {
                        Class<? extends Visit> selectedType = closeEvent.getValue("type"); (2)

                        if (selectedType != null) {
                            viewNavigators.detailView(this, selectedType) (3)
                                    .newEntity()
                                    .navigate();
                        }
                    }
                })
                .open();
    }

    private JmixSelect<Class<? extends Visit>> createSelectComponent() {
        JmixSelect<Class<? extends Visit>> select = uiComponents.create(JmixSelect.class);
        select.setRequired(true);
        select.setWidthFull();
        return select;
    }

    private String entityCaption(Class<? extends Visit> entityClass) {
        return messageTools.getEntityCaption(metadata.getClass(entityClass));
    }
}
1 The visitTypes map is created and passed as the items for the select component.
2 After the user confirms their selection, the selected visit type class is retrieved from the dialog’s result via closeEvent.getValue("type").
3 The viewNavigators.detailView method navigates to the detail view for the selected visit type, creating a new instance of the entity and showing its specific fields.

Using the type selection dialog, the application dynamically determines the appropriate subclass for the new Visit entity. After the user confirms their selection, Jmix navigates to the corresponding detail view for the chosen subclass. This is because we pass in the correct type into the detailView method of the ViewNavigators bean. The detail views are simply standard Jmix detail views for each subclass.

Regular Checkup Detail View with Relevant Fields
Figure 3. Regular Checkup Detail View

The RegularCheckup detail view includes the Next Appointment Date field, specific to this visit type. Common fields like Description and Visit Start, inherited from the Visit entity, are also displayed. When we look at the EmergencyVisit detail view, you can see the emergency-specific fields such as Emergency Type and a checkbox for Life Threatening:

Emergency Visit Detail View with Emergency-Specific Fields
Figure 4. Emergency Visit Detail View

Polymorphic Query in Visit List View

Let’s focus on how we display visits of all subtypes. As we learned before, all types of visits (RegularCheckup, EmergencyVisit, FollowUpVisit) are stored in a single table. In the view descriptor, we load the data as we would for any regular JPA entity. Below is an excerpt of the XML configuration for the VisitListView:

visit-list-view.xml
<collection id="visitsDc"
            class="io.jmix.petclinic.entity.visit.Visit"> (1)
    <fetchPlan extends="_base">
        <property name="pet" fetchPlan="_base"/>
    </fetchPlan>
    <loader id="visitsDl">
        <query>
            <![CDATA[select e from petclinic_Visit e]]> (2)
        </query>
    </loader>
</collection>
1 The collection element defines a CollectionContainer for the base class Visit. This container serves as the binding point for the data grid in the UI and holds all visit entities, regardless of their specific subclass.
2 The loader element specifies a CollectionDataLoader that uses the JPQL query SELECT e FROM petclinic_Visit e. This query fetches all rows from the Visit table, leveraging the discriminator column (dtype) to ensure JPA loads the correct subclass for each entity.
Visit List View
Figure 5. Visit List View

When the query is executed, JPA fetches all data from the Visit table and loads visits of all types. While the table displays only common columns shared across all visit types, the data loader transparently handles the polymorphic behavior, ensuring that each record is represented as its appropriate Java object.

When we enable SQL logging, we can observe the SQL query executed by JPA. Below is the formatted SQL query that corresponds to what is executed:

SELECT LIMIT 0 50
    visit.ID,
    visit.DTYPE,
    visit.CREATED_BY,
    visit.CREATED_DATE,
    visit.DELETED_BY,
    visit.DELETED_DATE,
    visit.DESCRIPTION,
    visit.LAST_MODIFIED_BY,
    visit.LAST_MODIFIED_DATE,
    visit.TREATMENT_STATUS,
    visit.TYPE_,
    visit.VERSION,
    visit.VISIT_END,
    visit.VISIT_START,
    visit.ASSIGNED_NURSE_ID,
    visit.PET_ID
FROM
    PETCLINIC_VISIT visit
WHERE
    visit.DELETED_DATE IS NULL
ORDER BY
    visit.VISIT_START DESC,
    visit.ID ASC

Visualize Subclass type in UI

When integrating entity hierarchies into the UI, the challenge arises of how to display and work with entity types. At first glance, you might consider leveraging the discriminator column (DType) used in the database. However, this column is not a true entity attribute. It lacks getters and setters, cannot be localized or translated, and is generally not intended for direct use in application logic. Additionally, it cannot be used in Jmix UI components such as the Generic Filter for searching, sorting, or filtering.

To address this limitation, we introduce a dedicated type field alongside the DType column. Represented as an regular Jmix Enum, this type field is mainly used for the UI and application code. It supports localization, enables filtering and sorting in polymorphic views, and provides a more developer-friendly interface for working with entity types.

To ensure that the type field reflects the correct subclass, we assign its value using a @PostConstruct lifecycle callback in each subclass. For instance, the RegularCheckup class assigns the type field as follows:

RegularCheckup.java
@JmixEntity
@Entity(name = "petclinic_RegularCheckup")
@DiscriminatorValue("REGULAR_CHECKUP")
public class RegularCheckup extends Visit {

    @PostConstruct
    public void initVisitType() {
        setType(VisitType.REGULAR_CHECKUP);
    }
}

With this in place for each subclass, we can use the type field is as a column as well as for filtering:

Visit List View with Type Filter
Figure 6. Visit List View with Type Filter

This wraps up our first entity inheritance approach: Single Table Inheritance. As we have seen, it provides a straightforward way to manage entity hierarchies in a single table. It allows polymorphic queries and avoids the need for multiple joins. However, there are limitations to this approach, particularly when subclass-specific fields are supposed to be required, as we discussed earlier.

To overcome this limitation, we can use the Joined inheritance strategy, which we will cover in the next section.

Joined Table Inheritance

Joined Table Inheritance maps an inheritance hierarchy into multiple tables. The base class has its own table, while each subclass has a separate table containing only fields specific to that subclass. Foreign keys connect the subclass tables to the base class table. This creates a more normalised database structure, as compared to Single Table Inheritance we don’t end up with a lot of null values in columns. It is also more flexible, making it suitable for scenarios where subclass-specific fields need database-level constraints, such as NOT NULL, or separate indexing.

For this guide, we picked the Pet entity as an example for the joined table inheritance. Let’s see at how the data model looks like.

Data Model

The Pet class represents the base entity with shared attributes like identificationNumber and birthdate, while the subclasses Bird and Cat define specific fields.

Joined Table Inheritance uses the @DiscriminatorColumn exacly how we have seen it before in the case of Single Table inheritance. The @DiscriminatorValue annotation in each subclass specifies the value used in the discriminator column to identify rows belonging to that entity type. Additionally, the subclasses have a @PrimaryKeyJoinColumn annotation that is required by JPA for performing the JOIN operation.

Pet.java
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.Table;

import java.time.LocalDate;

@JmixEntity
@Table(name = "PETCLINIC_PET")
@Entity(name = "petclinic_Pet")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE", discriminatorType = DiscriminatorType.STRING)
public class Pet extends NamedEntity {

    @Column(name = "IDENTIFICATION_NUMBER", nullable = false)
    @NotNull
    private String identificationNumber;

    @Column(name = "BIRTHDATE")
    private LocalDate birthdate;

    @JoinColumn(name = "TYPE_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private PetType type;

    @JoinColumn(name = "OWNER_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private Owner owner;

}

The Pet class contains attributes such as identificationNumber, birthdate, and associations with the Owner and PetType entities.

Bird.java
@JmixEntity
@Table(name = "PETCLINIC_BIRD")
@Entity(name = "petclinic_Bird")
@PrimaryKeyJoinColumn(name = "ID")
public class Bird extends Pet {
    @Column(name = "WINGSPAN", nullable = false)
    @NotNull
    private Double wingspan;

    public Double getWingspan() {
        return wingspan;
    }

    public void setWingspan(Double wingspan) {
        this.wingspan = wingspan;
    }
}

The Bird class adds the wingspan field, which is specific to birds.

Cat.java
@JmixEntity
@Table(name = "PETCLINIC_CAT")
@Entity(name = "petclinic_Cat")
@PrimaryKeyJoinColumn(name = "ID")
public class Cat extends Pet {
    @Column(name = "CLAW_LENGTH")
    private Integer clawLength;

    @Column(name = "LITTER_BOX_TRAINED", nullable = false)
    @NotNull
    private Boolean litterBoxTrained = false;

    public Boolean getLitterBoxTrained() {
        return litterBoxTrained;
    }

    public void setLitterBoxTrained(Boolean litterBoxTrained) {
        this.litterBoxTrained = litterBoxTrained;
    }

    public Integer getClawLength() {
        return clawLength;
    }

    public void setClawLength(Integer clawLength) {
        this.clawLength = clawLength;
    }
}

The Cat class includes fields like clawLength and litterBoxTrained, capturing attributes unique to cats.

Database Schema

In Joined Table Inheritance, the entity hierarchy is split into separate tables for the base class and each subclass. These tables are connected via a foreign key, enabling efficient joins when querying for subclass-specific data.

For the Pet hierarchy, the following tables are created:

Joined Table Inheritance Data Model
Figure 7. Joined Table Inheritance Data Model

When querying the base class (Pet), a Join is performed between the base table (PETCLINIC_PET) and all appropriate subclass tables (PETCLINIC_BIRD, PETCLINIC_CAT).

For example, if querying all pets via the Jmix DataManager API, the resulting SQL looks like this:

SELECT
    p.ID,
    p.IDENTIFICATION_NUMBER,
    p.BIRTHDATE,
    p.DTYPE,
    p.NAME,
    b.WINGSPAN,
    c.CLAW_LENGTH,
    c.LITTER_BOX_TRAINED
FROM PETCLINIC_PET p
LEFT JOIN PETCLINIC_BIRD b ON p.ID = b.ID
LEFT JOIN PETCLINIC_CAT c ON p.ID = c.ID;

The @PrimaryKeyJoinColumn annotation we already mentioned above establishes the relationship between the subclass tables and the base class table. This setup uses the primary key of the base class (ID) as the foreign key in the subclass tables. When executing queries involving joined inheritance, LEFT JOIN operations are performed between the base class table (PETCLINIC_PET) and each of the subclass tables (PETCLINIC_BIRD, PETCLINIC_CAT), ensuring all necessary data is combined for accurate representation of the inheritance hierarchy.

The use of LEFT JOIN is necessary because a record in the base class table (PETCLINIC_PET) corresponds to data in at most one of the subclass tables (PETCLINIC_BIRD, PETCLINIC_CAT) or none at all. This means that for a given base class record, there will typically be no data in the other subclass tables. A LEFT JOIN ensures that the query result includes the base class record even when no matching data is found in the subclass tables, preserving the integrity of the polymorphic query.

For example, the following result set might be produced by a query fetching all Pet entities:

Table 1. Result Set Example for Joined Inheritance
ID NAME IDENTIFICATION_NUMBER DTYPE WINGSPAN LITTER_BOX_TRAINED

1

Rowlet

722

petclinic_Bird

0.30

null

2

Zubat

041

petclinic_Bird

0.75

null

3

Crobat

169

petclinic_Bird

1.50

null

4

Houndour

228

petclinic_Cat

null

false

5

Rattata

019

petclinic_Cat

null

true

6

Meowth

052

petclinic_Cat

null

true

  • The DTYPE column indicates the subclass (petclinic_Bird or petclinic_Cat).

  • Fields like WINGSPAN and LITTER_BOX_TRAINED are only populated for rows that belong to their respective subclasses.

  • Null values in subclass-specific fields reflect that the corresponding record does not belong to that subclass.

Considerations for Joined Table Inheritance with Deep Hierarchies

When using Joined Table Inheritance, it’s important to understand the performance implications of having a large number of subtypes or multi-level inheritance hierarchies. As the number of subtypes and sub-subtypes increases, the complexity of SQL queries for polymorphic queries grows significantly. Each additional subtype or hierarchy level introduces a new LEFT JOIN operation, increasing the query execution time.

This warning is particularly relevant when performing polymorphic JPQL queries, such as SELECT e FROM petclinic_Pet e, which retrieve entities across the entire inheritance hierarchy. While databases are optimized for handling joins, large datasets combined with 10–15 or more subtypes or levels can lead to slow query performance, depending on indexing, amount of data and the database system in use.

For most use cases with a reasonable number of subtypes and properly indexed tables, Joined Table Inheritance performs efficiently. However, when working with a high number of subtypes or large datasets, consider alternative strategies like Single Table Inheritance or breaking the hierarchy into smaller structures to maintain acceptable query performance.

UI Representation

Now let’s look at how the Pet hierarchy is displayed in the Petclinic application. One of the key use cases is presenting pets in a list view, such as showing all pets for a specific owner. This list provides a unified view of all pets, regardless of their specific type (Bird, Cat, etc.).

The data grid contains, shared fields like Name, Identification Number, and Birthdate are displayed.

List of Pets for an Owner
Figure 8. List of Pets for an Owner

Dropdown for Creating Pets

Similar to the Visit example we need to determine which exact subclass should be used, when we create a Pet. This time we use a Dropdown Button for creating new pets in both the Owner Detail View and the Pet List View. Let’s take a look how the button is implemented in the Pet List View.

Pet List View with Dropdown Button
Figure 9. Pet List View with Dropdown Button

Below is the relevant portion of the pet-list-view.xml, where the dropdownButton and its actions are defined:

pet-list-view.xml
<hbox id="buttonsPanel" classNames="buttons-panel">
    <dropdownButton id="createBtn" icon="PLUS" text="msg://create" themeNames="icon"> (1)
    <items>
        <actionItem id="createPet" ref="petsDataGrid.createPet"/>
        <actionItem id="createCat" ref="petsDataGrid.createCat"/>
        <actionItem id="createBird" ref="petsDataGrid.createBird"/>
    </items>
    </dropdownButton>
    <button id="editBtn" action="petsDataGrid.edit"/>
    <button id="removeBtn" action="petsDataGrid.remove"/>
    <button id="excelExportBtn" action="petsDataGrid.excelExport"/>
    <simplePagination id="pagination" dataLoader="petsDl"/>
</hbox>
<dataGrid id="petsDataGrid"
          width="100%"
          minHeight="20em"
          dataContainer="petsDc">
    <actions>
        <action id="createPet" text="msg://pet"/>
        <action id="createCat" text="msg://createCat"/>
        <action id="createBird" text="msg://createBird"/>
        <action id="edit" type="list_edit"/>
        <action id="remove" type="list_remove"/>
        <action id="excelExport" type="grdexp_excelExport"/>
    </actions>
    <columns>
        <column property="name"/>
        <column property="identificationNumber"/>
        <column property="birthdate"/>
        <column property="type"/>
        <column property="owner"/>
    </columns>
</dataGrid>

The dropdownButton contains multiple actionItem elements, each linked to a specific pet type creation action. The actions are defined on the dataGrid and have custom implementations in the controller. Each action dynamically determines the correct detail view to open based on the selected pet type. Here is the relevant Java code from the Controller:

PetListView.java
@Subscribe("petsDataGrid.createPet")
public void onPetsDataGridCreatePet(final ActionPerformedEvent event) {
    viewNavigators.detailView(petsDataGrid)
            .withViewClass(PetDetailView.class)
            .newEntity()
            .navigate();
}

@Subscribe("petsDataGrid.createCat")
public void onPetsDataGridCreateCat(final ActionPerformedEvent event) {
    viewNavigators.detailView(petsDataGrid)
            .withViewClass(CatDetailView.class)
            .newEntity()
            .navigate();
}

@Subscribe("petsDataGrid.createBird")
public void onPetsDataGridCreateBird(final ActionPerformedEvent event) {
    viewNavigators.detailView(petsDataGrid)
            .withViewClass(BirdDetailView.class)
            .newEntity()
            .navigate();
}

By specifying the viewClass in the ViewNavigators call, we give Jmix the information which concrete subclass of Pet that is supposed to be instantiated.

Subclass-Specific Detail View

The screenshot above shows the detail view of a Bird entity. Similar to what we saw with Single Table Inheritance, this view displays both the attributes inherited from the base class (Pet) and subclass-specific fields.

Bird Detail View
Figure 10. Bird Detail View

One difference is that we are now able to create NOT NULL subclass fields. The wingspan field is marked as required, as indicated by the @NotNull constraint on the Bird entity class.

Table-Per-Class Inheritance

Table-Per-Class Inheritance maps each subclass to its own database table without using a shared base table. Each table contains the attributes specific to the subclass, including those inherited from the base class.

What’s the difference to a mapped superclass?

Both @MappedSuperclass and Table-Per-Class Inheritance involve dedicated tables for entities. However, Table-Per-Class Inheritance allows for polymorphic queries, which is not possible with a @MappedSuperclass. Additionally, in Table-Per-Class Inheritance, the superclass itself may also have its own table, depending on its configuration, enabling more flexible data organization.

This strategy can be appealing when queries are predominantly executed on specific subclasses, as it avoids the need for joins between a base table and subclass tables, simplifying queries and potentially improving performance for such cases. However, the trade-offs of this approach are significant. Queries that span multiple subclasses (polymorphic queries) require UNION operations across the subclass tables. These operations are computationally expensive and can lead to poor performance, especially with large datasets.

Limited EclipseLink Support

In Jmix, which uses EclipseLink as the JPA implementation, Table-Per-Class Inheritance faces additional limitations. Features like pagination, sorting, and filtering often do not work reliably with polymorphic queries under this strategy. These restrictions make it unsuitable for scenarios where polymorphic behavior or dynamic data retrieval is required.

In most cases, it is better to use other inheritance strategies:

  • Use @MappedSuperclass if polymorphic behavior is unnecessary, and queries always target specific subclasses. This approach keeps the schema simple and avoids database-level inheritance.

  • Use Joined Table Inheritance when polymorphic queries are needed and the downside of additional joins is acceptable.

  • Use Single Table Inheritance for maximum query performance, as all entities are stored in a single table, though it limits database constraints for subclass-specific fields.

Multilevel Inheritance

Although we only looked at one level-hierarchies, it’s worth mentioning that JPA also support multilevel inheritance. This means you can create deeper hierarchies with multiple levels of subclasses, such as having a Bird subclass of Pet, and further specialized subclasses like Waterbird or Raptor. Each level of the hierarchy can define its own fields and behaviors, and JPA will handle the mapping and querying appropriately based on the selected inheritance strategy.

Multilevel inheritance can be useful for highly detailed domain models where entities need to represent increasingly specific types. However, it’s important to carefully evaluate whether the added complexity aligns with the application’s requirements, as deeper hierarchies can lead to more complex queries and maintenance challenges.

It is important to note that within a single inheritance hierarchy, you cannot mix different JPA inheritance strategies. The strategy chosen at the top of the hierarchy applies to all levels below it. For instance, if you select the Joined Table inheritance strategy for a base class like Pet, that strategy will automatically extend to all subclasses, including multiple levels of subclasses such as Bird and Parrot.

The only exception to this rule is the use of @MappedSuperclass, which is not a true inheritance strategy but a way to define reusable fields and behaviors. If you observed closely, the Pet entity in our example above extends the NamedEntity class, which is annotated with @MappedSuperclass. This allows the Pet class to inherit common attributes like id and name without defining them again, while still defining an inheritance strategy (@Inheritance(strategy = InheritanceType.JOINED)) for its own hierarchy starting from Pet.

Summary

This guide explored how JPA inheritance strategies can model complex domains while bridging the gap between object-oriented programming and relational databases. We reviewed @MappedSuperclass for sharing common fields, Single Table Inheritance for simplicity and polymorphic queries, and Joined Table Inheritance for a more normalised schema as well as enforcing database-level constraints. Each approach was demonstrated with examples from the Petclinic application to showcase its strengths and trade-offs.

Inheritance strategies are powerful tools for reducing redundancy and aligning the domain model with business requirements. However, their effectiveness depends on selecting the right approach for the specific use case. Single Table Inheritance offers efficiency but limits constraints, while Joined Table Inheritance ensures data integrity with more complex queries. We also touched on Table-Per-Class Inheritance, noting its rare practicality due to significant limitations in performance and compatibility with frameworks like EclipseLink.

By understanding the trade-offs of each inheritance strategy, you can design maintainable, efficient domain models that align with their application’s needs. These techniques enable clear, flexible, and robust representations of hierarchical data, ensuring long-term scalability and maintainability for business applications.