Наследование сущностей с помощью Joined Table (пример на Hibernate и Spring Boot)

Наследовать сущности полезно не для того, чтобы хранить общую часть в родительской сущности (это реализуется композицией). А для того, чтобы можно было использовать их в шаблонах проектирования: например, Strategy. Ниже рассмотрим пример.
Допустим, у нас есть заказчики, для которых нужно рассчитать скидку:

  • EmployeeCustomer — это сотрудник, скидка которого зависит от времени, проведенного в компании.
  • ExternalCustormer — внешний заказчик, скидка его зависит от суммы заказов.

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

EmployeeCustomer и ExternalCustormer — наследники Customer.

Модель

Customer содержит общие данные, такие как имя (имя есть у всех):

@Entity
@Inheritance(
        strategy = InheritanceType.JOINED
)
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
    private String name;

   // getters/setters/constructors
}

Аннотация @Inheritance

Аннотация со значением InheritanceType.JOINED говорит о том, что будет общая таблица customer для хранения общих данных , плюс данные каждого наследника тоже будут храниться в отдельной таблице.

Наследники

Теперь наследники, первый EmployeeCustomer:

@Entity
@Data
public class EmployeeCustomer extends Customer {
    private int monthsInCompany;
   // getters/setters/constructor
}

где monthsInCompany — количество месяцев в компании.

И ExternalCustormer:

@Entity
public class ExternalCustomer extends Customer {
   private long sum;
  // getters/setters/constructor
}

где sum — сумма заказов, от нее зависит скидка.

Схема в базе данных

Вышеприведенные классы генерируют такую структуру в базе данных:

Структура БД
Структура БД

Первичные ключи employee_customer.id и external_customer.id являются заодно и внешними: они ссылаются на customer.id.

Расчет скидок с помощью шаблона Strategy

Суть шаблона состоит в том, что есть единая задача (рассчитать скидку). Она задается интерфейсом

public interface DiscountCalculator<T> {
    double calculate(T customer);
    Class<T> getClazz();
}

где T — конкретный класс заказчика.

Каждый класс, реализующий интерфейс DiscountCalculator,  использует свою стратегию для расчета скидки.

У сотрудника скидка зависит от числа отработанных месяцев:

@Service
public class EmployeeCustomerDiscountCalculator implements DiscountCalculator<EmployeeCustomer> {
    @Override
    public double calculate(EmployeeCustomer customer) {
        if (customer.getMonthsInCompany() > 12)
            return 0.1;
        return 0.05;
    }
    @Override
    public Class getClazz(){
        return EmployeeCustomer.class;
    }
}

А у внешних заказчиков от потраченной суммы:

@Service
public class ExternalCustomerDiscountCalculator implements DiscountCalculator<ExternalCustomer> {
    @Override
    public double calculate(ExternalCustomer customer) {
        if (customer.getSum() > 100)
            return 0.01;
        return 0.05;
    }
    @Override
    public Class getClazz(){
        return ExternalCustomer.class;
    }
}

Поиск заказчиков

Список всех заказчиков можно найти с помощью унаследованного стандартного метода findAll() репозитория:

public interface CustomerRepository extends JpaRepository<Customer, Long> {

}

findAll() найдет заказчиков всех типов: как ExternalCustomer, так и EmployeeCustomer, причем их можно привести к конкретному типу. Найдем всех заказчиков и рассчитаем для каждого скидку с помощью соответствующего (классу заказчика) алгоритма.

Делается это в методе makeDiscounts() ниже.

После инициализации бина MakeDiscountsService мы заполняем map, в котором для каждого класса заказчика записан свой калькулятор скидки. Это нужно, чтоб потом при переборе заказчиков для каждого из них применять нужный калькулятор.
@Service
public class MakeDiscountsService {
    @Autowired
    private CustomerRepository customerRepository;
    @Autowired
    private List<DiscountCalculator> calculators;

    private Map<Class, DiscountCalculator> map = new HashMap<>();

    @PostConstruct
    private void init() {
        for (DiscountCalculator discountCalculator : calculators) {
            map.put(discountCalculator.getClazz(), discountCalculator);
        }
    }

    public void makeDiscounts() {
        List<Customer> customers = customerRepository.findAll();
        for (Customer customer : customers) {
            DiscountCalculator discountCalculator = map.get(customer.getClass());
            double discount = discountCalculator.calculate(customer);
            System.out.println(discount);
        }
    }
}

Итоги

Мы рассмотрели тип наследования  JOINED table.

Также мы применили шаблон проектирования Strategy, который стал возможен благодаря наследованию. Если бы сущности не были унаследованы от общего Customer, то не получилось бы применить шаблон Strategy для расчета скидки.

Исходный код примера есть на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *