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:
-
Get 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.
-
You can download and unzip the source repository
-
Or clone it and switch to the
release_2_6branch:git clone https://github.com/jmix-framework/jmix-data-modeling-entity-inheritance-sample cd jmix-data-modeling-entity-inheritance-sample git checkout release_2_6
-
-
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
NamedEntitythat centralizes common fields likeid,name, and auditing attributes. Entities such asOwner,PetType, andSpecialtyinherit these fields. -
Single Table Inheritance (Visit Hierarchy): The
Visitbase class encompasses subclasses likeRegularCheckup,EmergencyVisit, andFollowUp. All visits are stored in a single table, enabling efficient polymorphic queries and simplifying associations. -
Joined Table Inheritance (Pet Hierarchy): The
Petbase class includes subclasses likeCatandBird. 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
nameandbirthDate, but specific types, such as cats and birds, introduce unique fields likelitterBoxTrainedorwingspan. Inheritance allows these specialized attributes to coexist with common ones. -
Visits: Different types of visits, such as
RegularCheckuporEmergencyVisit, require additional fields specific to their type. At the same time, all visits share common properties likevisitStartandtreatmentStatus, 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.
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.
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 |
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:
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:
@MappedSuperclass)SELECT n
FROM petclinic_NamedEntity n
WHERE n.name = :name
Instead, queries have to be performed on the specific subclasses. For example:
name field in SpecialtySELECT 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 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 |
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.
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. |
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);
}
}
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:
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.
Here is how this is implemented in the controller when creating a new visit:
@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.
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:
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:
<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. |
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:
@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:
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.
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.
@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.
@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:
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:
| 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
DTYPEcolumn indicates the subclass (petclinic_Birdorpetclinic_Cat). -
Fields like
WINGSPANandLITTER_BOX_TRAINEDare 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 This warning is particularly relevant when performing polymorphic JPQL queries, such as 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.
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.
Below is the relevant portion of the pet-list-view.xml, where the dropdownButton and its actions are defined:
<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:
@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.
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 |
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
@MappedSuperclassif polymorphic behavior is unnecessary, and queries always target specific subclasses. This approach keeps the schema simple and avoids database-level inheritance. -
Use
Joined Table Inheritancewhen polymorphic queries are needed and the downside of additional joins is acceptable. -
Use
Single Table Inheritancefor 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.







