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:
-
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.
-
Download and unzip the source repository
-
or clone it using git:
git clone https://github.com/jmix-framework/jmix-data-modeling-entity-inheritance-sample.git
-
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 likeid
,name
, and auditing attributes. Entities such asOwner
,PetType
, andSpecialty
inherit these fields. -
Single Table Inheritance (Visit Hierarchy): The
Visit
base 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
Pet
base class includes subclasses likeCat
andBird
. 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
andbirthDate
, but specific types, such as cats and birds, introduce unique fields likelitterBoxTrained
orwingspan
. Inheritance allows these specialized attributes to coexist with common ones. -
Visits: Different types of visits, such as
RegularCheckup
orEmergencyVisit
, require additional fields specific to their type. At the same time, all visits share common properties likevisitStart
andtreatmentStatus
, 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 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 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
DTYPE
column indicates the subclass (petclinic_Bird
orpetclinic_Cat
). -
Fields like
WINGSPAN
andLITTER_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 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
@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.