1. Introduction
Inheritance and composition are two fundamental concepts in object-oriented programming (OOP) that we can also leverage in JPA for data modeling. In JPA, both inheritance and composition are techniques for modeling relationships between entities, but they represent different kinds of relationships. In this tutorial, we’ll explore both approaches and their implications.
2. Inheritance in JPA
Inheritance represents an “is-a” relationship, where a subclass inherits properties and behaviors from a superclass. This promotes code reuse by allowing subclasses to inherit attributes and methods from a superclass. JPA offers several strategies to model inheritance relationships between our entities and their corresponding database tables.
2.1. Single Table Inheritance (STI)
Single Table Inheritance (STI) involves mapping all subclasses to a single database table. This simplifies schema management and query execution by utilizing discriminator columns to differentiate between subclass instances.
We start by defining the Employee entity class as the superclass using the @Entity annotation. Next, we set the inheritance strategy to InheritanceType.SINGLE_TABLE, so it maps all the subclasses to the same database table.
Then, we use the @DiscriminatorColumn annotation to specify the discriminator column in the Employee class. This column is used to differentiate between different types of entities in a single table.
In our example, we use name = “employee_type” to specify the column name as “employee_type” and discriminatorType = DiscriminatorType.STRING to indicate that it contains string values:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "employee_type", discriminatorType = DiscriminatorType.STRING)
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Getters and setters
}
For each subclass, we use the @DiscriminatorValue annotation to specify the value of the discriminator column that corresponds to that subclass. In our example, we use “manager” and “developer” as discriminator values for the Manager and Developer subclasses, respectively:
@Entity
@DiscriminatorValue("manager")
public class Manager extends Employee {
private String department;
// Getters and setters
}
@Entity
@DiscriminatorValue("developer")
public class Developer extends Employee {
private String programmingLanguage;
// Getters and setters
}
Here’s an example of the SQL statement logged to the console when we run the Spring application:
Hibernate:
create table Employee (
id bigint generated by default as identity,
employee_type varchar(31) not null,
department varchar(255),
name varchar(255),
programmingLanguage varchar(255),
primary key (id)
)
This approach is ideal for inheritance hierarchies where most subclasses share a similar set of attributes, and it reduces table creation and the number of querying. However, this may lead to sparse tables and potential performance issues as the hierarchy expands.
2.2. Joined Table Inheritance (JTI)
On the other hand, Joined Table Inheritance (JTI) splits subclasses into their tables. Each subclass gets its own table to store its unique details. Additionally, there’s another table to hold shared information common to all subclasses.
Let’s illustrate this concept with our superclass, Vehicle, which encapsulates attributes common to both cars and bikes:
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Vehicle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String brand;
private String model;
// Getters and setters
}
@Entity
public class Car extends Vehicle {
private int numberOfDoors;
// Getters and setters
}
@Entity
public class Bike extends Vehicle {
private boolean hasBasket;
// Getters and setters
}
In this setup, every subclass (Car and Bike) has its own table within the database to accommodate its unique attributes. However, the Vehicle superclass doesn’t possess a dedicated table. Instead, Vehicle as a distinct table serves to store common information, such as brand and model, shared among all subclasses:
Hibernate:
create table Bike (
hasBasket boolean not null,
id bigint not null,
primary key (id)
)
Hibernate:
create table Car (
numberOfDoors integer not null,
id bigint not null,
primary key (id)
)
Hibernate:
create table Vehicle (
id bigint generated by default as identity,
brand varchar(255),
model varchar(255),
primary key (id)
)
When we query data, we may need to join the tables together to retrieve information about specific vehicles. Because both Bike and Car inherit the id from the superclass table (Vehicle) the id column in both Car and Bike will be the foreign key referencing the id in Vehicle table.
This approach is suitable when subclasses have significant differences in attributes, minimizing redundancy in the main table.
2.3. Table per Class Inheritance (TPC)
When using the Table per Class (TPC) inheritance strategy in JPA, each class in the inheritance hierarchy corresponds to its dedicated database table. Consequently, we see separate tables created for each class: Shape, Square, and Circle. This differs from JTI, where each subclass has its own table to store its unique details, and shared attributes are stored in a joined table.
Let’s consider a scenario with shapes like circles and squares. We can model this using TPC:
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Shape {
@Id
private Long id;
private String color;
// Getters and setters
}
@Entity
public class Circle extends Shape {
private double radius;
// Getters and setters
}
@Entity
public class Square extends Shape {
private double sideLength;
// Getters and setters
}
For the Shape table, columns for id and color are created, representing the shared attributes inherited by both Square and Circle:
Hibernate:
create table Shape (
id bigint not null,
color varchar(255),
primary key (id)
)
Hibernate:
create table Square (
sideLength float(53) not null,
id bigint not null,
color varchar(255),
primary key (id)
)
Hibernate:
create table Circle (
radius float(53) not null,
id bigint not null,
color varchar(255),
primary key (id)
)
While TPC inheritance provides a clear mapping between classes and tables, it inherits all shared attributes into the subclasses’ tables. This approach can lead to data redundancy, as each subclass table duplicates the columns inherited from the superclass table. This redundancy results in larger database sizes and increased storage requirements.
Additionally, updates to shared attributes in the superclass may necessitate modifications to multiple tables, potentially posing maintenance challenges.
3. Composition in JPA
The composition represents a “has-a” relationship, where one object contains another as a component part. In JPA, composition is often implemented using entity relationships, such as one-to-one, one-to-many, or many-to-many associations. In contrast to inheritance, composition allows for more flexible and loosely coupled relationships between entities.
Let’s illustrate each type of composition relationship in JPA with examples.
3.1. One-to-One Composition
In a one-to-one composition relationship, one entity contains exactly one instance of another entity as its component part. This is typically modeled using a foreign key in the owning entity’s table to reference the primary key of the associated entity.
Let’s consider a Person entity that has a one-to-one composition relationship with an Address entity. Each person has exactly one address:
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id")
private Address address;
// Getters and setters
}
The Address entity represents the physical location details:
@Entity
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String street;
private String city;
private String zipCode;
// Getters and setters
}
In this scenario, if we decide to add a new field to the Address entity (e.g., country), we can do so independently without any changes to the Person entity. However, in an inheritance relationship, adding a new field in the superclass may indeed require modifications to the subclass tables. This highlights one of the advantages of composition over inheritance in terms of flexibility and ease of maintenance.
3.2. One-to-Many Composition
In a one-to-many composition relationship, one entity contains a collection of instances of another entity as its parts. This is typically modeled using a foreign key in the “many” side entity’s table to reference the primary key of the “one” side entity.
Let’s say we have a Department entity that has a one-to-many composition relationship with Employee entities. Each department can have multiple employees:
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees;
// Getters and setters
}
The Employee entity contains a reference to the Department entity to which it belongs. This is represented by the @ManyToOne annotation on the department field in the Employee entity:
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
// Getters and setters
}
3.3. Many-to-Many Composition
In a many-to-many composition relationship, entities from both sides contain collections of instances of the other entity as their component parts. This is typically modeled using a join table in the database to represent the association between the entities.
Let’s consider a Course entity that has a many-to-many composition relationship with Student entities. Each course can have multiple students, and each student can enroll in multiple courses. To model this in JPA, we use the @ManyToMany annotation in both the Course and Student entities:
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "courses")
private List<Student> students;
// Getters and setters
}
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses;
// Getters and setters
}
4. Summary
This table highlights the main differences between inheritance and composition, including their nature of relationship, code reusability, flexibility, and coupling:
Aspect | Inheritance | Composition |
---|---|---|
Nature of Relationship | Signifies an “is-a” relationship. | Represents a “has-a” relationship. |
Code Reusability | Facilitates code reuse within a hierarchy. Subclasses inherit behavior and attributes from superclasses. | Components can be reused in different contexts without the tight coupling inherent in inheritance. |
Flexibility | Changing the superclass may impact all subclasses, potentially leading to cascading changes. | Changes to individual components don’t affect the containing objects. |
Coupling | Tight coupling between classes. Subclasses are tightly bound to the implementation details of superclasses. | Looser coupling. Components are decoupled from the containing objects, reducing dependencies. |
5. Conclusion
In this article, we explored the fundamental differences between inheritance and composition in JPA entity modeling.
Inheritance offers code reusability and a clear hierarchical structure, making it suitable for scenarios where subclasses share common behavior and attributes. On the other hand, composition provides greater flexibility and adaptability, allowing for dynamic object assembly and reduced dependencies between components.
As always, the source code for the examples is available over on GitHub.