Inheritance vs. Composition in JPA

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.

item picture

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).

subject field model

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.

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.

break the hierarchy

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.

configuration strategy

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.

Implementation of installation across interfaces

@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.

.

Leave a Comment