an introduction
“Don’t repeat yourself” or “Dry”. Developers try to adhere to this principle while developing software. It helps to avoid writing redundant code and, as a result, simplifies the possibility of its maintenance in the future. But how can this principle be realized in the world of JPA?
There are two ways: inheritance and composition. Each has its drawbacks and advantages. Let’s figure out what they are in the not entirely “realistic” representative example.
subject area
Our form contains three entities: the article, the author, and the spectator. Each entity has fields for checking (created date, created date, modified date, modified date). The author and spectator also have fields for the address (country, city, street, building).
Inheritance: MappedSuperclass
To comply with the DRY principle, let’s take the duplicate fields into separate Mapped Superclasses. We will inherit our entities from them. Since all entities must have fields to audit, let’s start with the BaseEntityAudit class. We will create a class ‘BaseEntityAuditAddress’ for entities with address fields and inherit it from the BaseEntityAudit class.
Note: All methods in this article have been implemented and made 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 {
//...
}
Hierarchy of entities is implemented so that we don’t repeat ourselves anymore. Completed task. But what if…?
break the hierarchy
But what if the initial requirements for our model changed a bit? For example, consider that the article only needs proofreading fields, the viewer only needs title fields, and the author needs both. In this case, following the inheritance strategy, we will have to neglect the DRY principle anyway because there are no multiple legacies of classes in Java. In other words, our hierarchy will look like the diagram below, which is impossible to implement in Java.
We will have to leave two pre-created super chapters and one with title fields only for the viewer. Therefore, the address fields will be duplicated in two entities. If we want to comply with the DRY principle, let’s use combination instead.
to express: @Embeddable
and interfaces
Let’s implement configuration via interfaces using only one method: getBaseEntityAudit() or getBaseEntityAddress(). As you can guess, they will return the embeddable entities that contain the corresponding fields. Implementation of these methods in entities will replace CROP @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 extension @Embedded
Attribute and holder.
@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 Parent Class
We’ve achieved DRY in entity code, but what about working code that works with these entities? Let’s imagine that we need a method that returns the list of countries from the list of entities. In our example with inheritance, we will need to pass a list of type BaseEntityAuditAddress 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 as follows:
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());
}
}
The method may have become easier to read since we are explicitly referring to an entity that has only the address and not both the address and the audit fields.
conclusion
Ultimately, the syntax appears to have more flexible use cases. But even if you decide to use inheritance (one possible reason: intentionally limiting this flexibility), JPA Buddy will help you no matter what approach is chosen. Check it out in a short video version of this article.
.