Наследование и композиция в JPA


Введение

«Не повторяйся» или «DRY». Разработчики стараются придерживаться этого принципа при разработке программного обеспечения. Он помогает избежать написания избыточного кода и, как следствие, упрощает его сопровождаемость в будущем. Но как достичь этого принципа в мире JPA?

Существует два подхода: Наследование и Композиция. Оба имеют свои плюсы и минусы. Давайте разберемся, в чем они заключаются, на не совсем «реальном», но показательном примере.

Домен субъекта

В нашей модели есть три сущности: Статья, Автор и Зритель. Каждая сущность имеет поля для аудита (createdDate, createdBy, modifiedDate и modifedBy). Автор и Зритель также имеют поля для адреса (страна, город, улица, здание).

Наследование: MappedSuperclass .

Чтобы соблюсти принцип DRY, давайте вынесем дублирующие поля в отдельные суперклассы Mapped. От них мы будем наследовать наши сущности. Поскольку все сущности должны иметь поля для аудита, начнем с класса BaseEntityAudit. Мы создадим класс «BaseEntityAuditAddress» для сущностей с полями адреса и унаследуем его от класса BaseEntityAudit.

ПРИМЕЧАНИЕ: Все подходы, представленные в этой статье, реализованы и доступны в этом репозитории на 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 {
  //...
}
Вход в полноэкранный режим Выход из полноэкранного режима

Иерархия сущностей реализована так, чтобы мы больше не повторялись. Миссия выполнена. Но что если…

Нарушение иерархии

Но что, если исходные требования к нашей модели немного изменятся? Например, предположим, что статье нужны только поля аудита, зрителю — только поля адреса, а автору — оба поля. В этом случае, следуя стратегии наследования, нам все равно придется пренебречь принципом DRY, поскольку в Java не существует множественного наследования для классов. Другими словами, наша иерархия будет выглядеть как на диаграмме ниже, что невозможно реализовать в Java.

Нам придется оставить два суперкласса, созданных ранее, и один с полями адреса только для Spectator. Таким образом, адресные поля будут повторяться в двух сущностях. Если мы хотим соблюсти принцип DRY, давайте вместо этого воспользуемся композицией.

Композиция: @Embeddable и интерфейсы

Давайте реализуем композицию через интерфейсы, имеющие только один метод: getBaseEntityAudit() или getBaseEntityAddress(). Как вы можете догадаться, они будут возвращать встраиваемые сущности, содержащие соответствующие поля. Реализация этих методов в сущностях заменит геттеры для полей @Embedded.

@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(); 
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем свободно использовать эти интерфейсы в любой сущности. Чтобы реализовать методы интерфейса, нужно добавить атрибут @Embedded и геттер для него.

@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; 
}
//... 
}
Вход в полноэкранный режим Выход из полноэкранного режима

Полиморфизм: апкаст на родительский класс

Мы добились DRY в коде сущностей, но что насчет бизнес-кода, который работает с этими сущностями? Представим, что нам нужен метод, который возвращает список стран из списка сущностей. В нашем примере с наследованием нам нужно будет передать список с типом BaseEntityAuditAddress в качестве параметра. И мы сможем использовать этот метод как для Авторов, так и для Зрителей.

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()); 
    } 
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Использование будет выглядеть следующим образом:

List<BaseEntityAuditAddress> authors = new ArrayList<>();

//add authors to the list

List<String> countries = new Business().getCountries(authors);
Войти в полноэкранный режим Выйти из полноэкранного режима

Однако изменение подхода ничего не изменит. Все, что нужно изменить, это заменить BaseEntityAuditAddress на 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());
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Возможно, метод стал более удобным для чтения, поскольку мы явно ссылаемся на сущность только с адресом, а не с полями адреса и аудита.

Заключение

В конечном итоге, композиция кажется более гибким вариантом использования. Но даже если вы решите использовать наследование (одна из возможных причин: намеренное ограничение такой гибкости), JPA Buddy поможет вам независимо от выбранного подхода. Проверьте это в короткой видеоверсии этой статьи.

Оцените статью
devanswers.ru
Добавить комментарий