Рубрики
Без рубрики

Лучший способ использовать наследование сущностей с JPA и гибернацией

Узнайте, как лучше всего использовать наследование сущностей с помощью JPA и гибернации. Наследование полезно при реализации поведенческих моделей, таких как стратегия или посетитель.

Автор оригинала: Vlad Mihalcea.

Вступление

Недавно мой друг Лукас Эдер написал следующее сообщение в Twitter:

5. Вам не нужно наследование. Особенно не наследование сущностей.

Как и в любом языке ООП (объектно-ориентированного программирования), наследование сущностей подходит для изменения поведения, а не для повторного использования структур данных, для которых мы могли бы использовать композицию. Модель предметной области, ставящая под угрозу как данные (например, сохраняемые сущности), так и поведение (бизнес-логику), мы все еще можем использовать наследование для реализации поведенческого шаблона проектирования программного обеспечения.

В этой статье я собираюсь продемонстрировать, как использовать наследование JPA в качестве средства реализации шаблона Разработки стратегии .

Модель предметной области

Учитывая, что у нас есть система уведомлений, которая должна отправлять клиентам как электронную почту, так и SMS, мы можем смоделировать отношения уведомлений следующим образом:

Как Sms - уведомление , так и Уведомление по электронной почте наследуют базовый класс Уведомление свойства. Однако, если мы используем СУБД (систему реляционных баз данных), стандартного способа реализации наследования таблиц не существует, поэтому нам необходимо эмулировать эту взаимосвязь. Как правило, есть только два варианта:

  • либо мы используем одну таблицу, но тогда нам нужно убедиться, что все ограничения NOT NULL выполняются с помощью ПРОВЕРКИ ТРИГГЕРА
  • или мы можем использовать отдельные таблицы для сущностей базового класса и подкласса, в этом случае Первичный ключ таблицы подкласса также является внешним ключом к Первичному ключу базового класса.

В этом примере мы будем использовать подход ОБЪЕДИНЕННОЙ таблицы, который имеет следующую диаграмму отношений сущностей базы данных:

Преодоление разрыва

С помощью JPA и Hibernate сопоставление моделей ООП и СУБД является простым.

Базовый класс Notification отображается следующим образом:

@Entity
@Table(name = "notification")
@Inheritance(
    strategy = InheritanceType.JOINED
)
public class Notification {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Temporal( TemporalType.TIMESTAMP )
    @CreationTimestamp
    @Column(name = "created_on")
    private Date createdOn;

    //Getters and setters omitted for brevity
}

Sms-уведомление и Уведомление по электронной почте сопоставления выглядят следующим образом:

@Entity
@Table(name = "sms_notification")
public class SmsNotification 
    extends Notification {

    @Column(
        name = "phone_number", 
        nullable = false
    )
    private String phoneNumber;

    //Getters and setters omitted for brevity
}
@Entity
@Table(name = "email_notification")
public class EmailNotification 
    extends Notification {

    @Column(
        name = "email_address", 
        nullable = false
    )
    private String emailAddress;

    //Getters and setters omitted for brevity
}

Бизнес-логика

До сих пор мы только отображали взаимосвязь между HOOP и структурами данных СУБД, но мы не рассмотрели фактическую бизнес-логику, необходимую для отправки этих уведомлений нашим пользователям.

Для этой цели у нас есть следующие Отправитель уведомлений Сервисные компоненты:

Отправитель Уведомления имеет два метода:

  • применяется к предоставляет сущность, поддерживаемую этим Отправителем уведомления
  • отправить инкапсулирует фактическую логику отправки

Отправитель Уведомления по электронной почте реализован следующим образом:

@Component
public class EmailNotificationSender 
    implements NotificationSender {

    protected final Logger LOGGER = LoggerFactory.getLogger( 
        getClass() 
    );

    @Override
    public Class appliesTo() {
        return EmailNotification.class;
    }

    @Override
    public void send(EmailNotification notification) {
        LOGGER.info( 
            "Send Email to {} {} via address: {}",
             notification.getFirstName(),
             notification.getLastName(),
             notification.getEmailAddress()
        );
    }
}

Конечно, фактическая логика отправки была удалена, но этого достаточно, чтобы понять, как работает шаблон стратегии.

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

Поэтому мы можем использовать шаблон Фасад для предоставления очень простого API:

Отправитель уведомлений Impl – это место, где происходит все волшебство:

@Service
public class NotificationServiceImpl 
    implements NotificationService {

    @Autowired
    private NotificationDAO notificationDAO;

    @Autowired
    private List notificationSenders;

    private Map, NotificationSender> 
        notificationSenderMap = new HashMap<>();

    @PostConstruct
    @SuppressWarnings( "unchecked" )
    public void init() {
        for ( NotificationSender notificationSender : notificationSenders ) {
            notificationSenderMap.put( 
                notificationSender.appliesTo(), 
                notificationSender 
            );
        }
    }

    @Override
    @Transactional
    @SuppressWarnings( "unchecked" )
    public void sendCampaign(String name, String message) {
        List notifications = notificationDAO.findAll();

        for ( Notification notification : notifications ) {
            notificationSenderMap
                .get( notification.getClass() )
                .send( notification );
        }
    }
}

В этой реализации следует отметить несколько моментов:

  • Мы используем функцию Spring List автоматической проводки, которую я объяснил в моем самом первом посте в блоге . Таким образом, мы можем ввести любого Отправителя уведомлений , настроенного пользователем в нашей системе, таким образом, отделив Службу уведомлений от фактических Отправителей уведомлений реализаций, которые в настоящее время поддерживает наша система.
  • Метод init создает карту Отправителя уведомлений , которая принимает тип Уведомления класса в качестве ключа Карты и связанный Отправитель уведомлений в качестве значения Карты .
  • Метод отправить кампанию извлекает Список объектов Уведомлений со уровня DAO и отправляет их в связанные Отправитель уведомлений экземпляры.

Поскольку JPA предлагает полиморфные запросы, метод findAll DAO может быть реализован следующим образом:

@Override
public List findAll() {
    CriteriaBuilder builder = entityManager
        .getCriteriaBuilder();
        
    CriteriaQuery criteria = builder
        .createQuery( entityClass );
    criteria.from( entityClass );

    return entityManager
        .createQuery( criteria )
        .getResultList();
}

Написание запросов API критериев JPA не очень просто. Плагин Codota IDE может помочь вам в написании таких запросов, что повысит вашу производительность.

Для получения более подробной информации о том, как вы можете использовать Codota для ускорения процесса написания запросов API критериев, ознакомьтесь с этой статьей .

Системе не обязательно знать, какие фактические Уведомления реализации выбрал каждый клиент. Полиморфный запрос вычисляется во время выполнения JPA и Hibernate.

Время тестирования

Если мы создали следующие Уведомления объекты в нашей системе:

SmsNotification sms = new SmsNotification();
sms.setPhoneNumber( "012-345-67890" );
sms.setFirstName( "Vlad" );
sms.setLastName( "Mihalcea" );

entityManager.persist( sms );

EmailNotification email = new EmailNotification();
email.setEmailAddress( "vlad@acme.com" );
email.setFirstName( "Vlad" );
email.setLastName( "Mihalcea" );

entityManager.persist( email );

А теперь мы хотим отправить кампанию:

notificationService.sendCampaign( 
    "Black Friday", 
    "High-Performance Java Persistence is 40% OFF" 
);

Hibernate выполняет следующий SQL-запрос:

SELECT  
    n.id AS id1_1_,
    n.created_on AS created_2_1_,
    n.first_name AS first_na3_1_,
    n.last_name AS last_nam4_1_,
    n1_.email_address AS email_ad1_0_,
    n2_.phone_number AS phone_nu1_2_,
    CASE WHEN n1_.id IS NOT NULL THEN 1
         WHEN n2_.id IS NOT NULL THEN 2
         WHEN n.id IS NOT NULL THEN 0
    END AS clazz_
FROM    
    notification n
LEFT OUTER JOIN 
    email_notification n1_ ON n.id = n1_.id
LEFT OUTER JOIN 
    sms_notification n2_ ON n.id = n2_.id

И регистрируется следующий вывод:

EmailNotificationSender - Send Email to Vlad Mihalcea via address: vlad@acme.com

SmsNotificationSender - Send SMS to Vlad Mihalcea via phone number: 012-345-67890

Круто, правда?

Вывод

Наследование сущностей-очень полезный метод, но только когда вы используете его вместе с шаблоном разработки поведенческого программного обеспечения, таким как Стратегия или Шаблон посетителя .

Если вам нужно только распространить определенные свойства из базового класса на все подклассы, вам не нужно наследование сущностей JPA. Все, что вам нужно, – это аннотация @MappedSuperclass , но это не наследование сущностей, поскольку иерархия объектов видна только в домене ООП, а не в модели отношений.