Автор оригинала: 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 ListnotificationSenders; 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 ListfindAll() { 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
, но это не наследование сущностей, поскольку иерархия объектов видна только в домене ООП, а не в модели отношений.