Inheritance vs Composition in JPA
Georgii Vlasov
Posted on August 26, 2022
Introduction
«Don't repeat yourself» or «DRY». Developers try to adhere to this principle during software development. It helps to avoid redundant code writing and, as a result, simplifies its maintainability in the future. But how to achieve this principle in the JPA world?
There are two approaches: Inheritance and Composition. Both have their pros and cons. Let's figure out what they are on the not quite "real-world" but representative example.
Subject Domain
Our model has three entities: Article, Author, and Spectator. Each entity has fields for audit (createdDate, createdBy, modifiedDate, and modifedBy). Author and Spectator also have fields for address (country, city, street, building).
Inheritance: MappedSuperclass
To comply with the DRY principle, let's take duplicate fields to the separate Mapped Superclasses. We will inherit our entities from them. Since all entities must have fields for auditing, let's start with the BaseEntityAudit class. We will create a "BaseEntityAuditAddress" class for entities with address fields and inherit it from the BaseEntityAudit class.
NOTE: All the approaches presented in this article are implemented and available in this repository on GitHub.
@MappedSuperclass
public class BaseEntityAuditAddress extends BaseEntityAudit {
@Column(name = "country")
private String country;
@Column(name = "city")
private String city;
@Column(name = "street")
private String street;
@Column(name = "building")
private String building;
//...
}
@Entity
@Table(name = "spectator")
public class Spectator extends BaseEntityAuditAddress {
//...
}
The hierarchy of entities is implemented so that we no longer repeat ourselves. Mission complete. But what if...
Breaking the hierarchy
But what if the initial requirements for our model change a little? For example, consider that Article needs just audit fields, Spectator needs address fields only, and Author needs both. In this case, following the Inheritance strategy, we will have to neglect the DRY principle anyway because there is no multiple inheritance for classes in Java. In other words, our hierarchy will look like a diagram below, which is impossible to implement in Java.
We will have to leave two Superclasses created earlier and one with address fields only for Spectator. So, the address fields will be repeated in two entities. If we want to comply with the DRY principle, let's use the composition instead.
Composition: @Embeddable
and Interfaces
Let’s implement the composition via interfaces with only one method: getBaseEntityAudit() or getBaseEntityAddress(). As you can guess, they will return embeddable entities containing the corresponding fields. Implementing these methods in entities will replace getters for @Embedded
fields.
@Embeddable
public class BaseEntityAudit {
@Column(name = "created_date", nullable = false, updatable = false)
@CreatedDate
private long createdDate;
@Column(name = "created_by")
@CreatedBy
private String createdBy;
@Column(name = "modified_date")
@LastModifiedDate
private long modifiedDate;
@Column(name = "modified_by")
@LastModifiedBy
private String modifiedBy;
// ...
}
public interface EntityWithAuditFields {
BaseEntityAudit getBaseEntityAudit();
}
Now we are free to use those interfaces in any entity. To implement interface methods, you need to add an @Embedded
attribute and a getter for it.
@Entity
@Table(name = "author")
public class Author implements EntityWithAuditFields, EntityWithAddressFields {
//...
@Embedded
private BaseEntityAudit baseEntityAudit;
@Embedded
private BaseEntityAddress baseEntityAddress;
public BaseEntityAddress getBaseEntityAddress() {
return baseEntityAddress;
}
public BaseEntityAudit getBaseEntityAudit() {
return baseEntityAudit;
}
//...
}
Polymorphism: upcast to the parent class
We have achieved DRY in the entity code, but what about business code that works with these entities? Let's imagine that we need a method that will return a list of countries from a list of entities. In our example with inheritance, we will need to pass a list with the BaseEntityAuditAddress type as a parameter. And we will be able to use this method for both Authors and Spectators.
public class Business {
public List<String> getCountries(List<BaseEntityAuditAddress> entitiesList) {
if (entitiesList == null || entitiesList.isEmpty()) {
return Collections.emptyList();
}
return entitiesList.stream()
.map(BaseEntityAuditAddress::getCountry)
.distinct()
.collect(Collectors.toList());
}
}
The usage will be the following:
List<BaseEntityAuditAddress> authors = new ArrayList<>();
//add authors to the list
List<String> countries = new Business().getCountries(authors);
However, changing the approach will not change anything. All that needs to be changed is to replace BaseEntityAuditAddress with EntityWithAddressFields.
public class Business {
public List<String> getCountries(List<EntityWithAddressFields> entitiesList) {
if (entitiesList == null || entitiesList.isEmpty()) {
return Collections.emptyList();
}
return entitiesList.stream()
.map(EntityWithAddressFields::getBaseEntityAddress)
.map(BaseEntityAddress::getCountry)
.distinct()
.collect(Collectors.toList());
}
}
Perhaps the method has become easier to read since we explicitly refer to an entity with only the address and not both the address and the audit fields.
Conclusion
In the end, the composition seems to have more flexible use cases. But even if you decide to use inheritance (one of the possible reasons: to limit such flexibility intentionally), JPA Buddy will help you regardless of the chosen approach. Check it out in a short video version of this article.
Posted on August 26, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.