понедельник, 21 июля 2014 г.

О спорном паттерне DAO

В последние годы, после выхода спецификации Java EE 6, среди разработчиков и архитекторов информационных систем развернулась нешуточная дискуссия на тему паттерна DAO. Некоторые архитекторы и евангелисты уверены, что данный паттерн устарел и является избыточным решением в эпоху инъектируемого сразу в EJB- или CDI-компоненты JPA EntityManager'а. Другие же упорно настраивают на необходимости его применения.


@Stateless
public class MyDocumentService implements DocumentService {

    @PersistenceContext
    private EntityManager em;

    // ...
}

Давайте попробуем разобраться в данном вопросе.




Паттерн DAO предназначен для отделения взаимодействия с хранилищем данных от бизнес-логики приложения. Т.е. вся логика, отвечающая за сохранение, изменение, извлечение сущностей выносится в отдельные DAO-классы, а код, инкапсулирующий бизнес-логику приложения (т.н. сервисы), взаимодействует с этими классами, а не непосредственно с хранилищем данных. Такой подход обеспечивает гибкость в выборе подсистемы хранения для приложения. Если необходимо перейти с использования ORM на прямое взаимодействие с базой данных посредством JDBC или отказаться от использования внутреннего хранилища данных для приложения и перейти на взаимодействие с системой управления мастер-данными посредством веб-сервисов, то достаточно только заменить реализацию DAO.

Все прекрасно и звучит красиво, но как известно бесплатного супа не бывает. Поддержка конструкции "интерфейс сервиса - сервис - интерфейс DAO - одна или несколько реализаций DAO" довольно утомительна. Чтобы добавить новую операцию в сервис, в общем случае необходимо сделать следующее:

  1. Добавить метод в интерфейс сервиса.

  2. Добавить метод в реализацию сервиса.

  3. Добавить метод в интерфейс DAO.

  4. Добавить метод в одну или несколько реализаций DAO.


При этом большинство методов сервисов будут иметь подобную реализацию:


public class MyDocumentService implements DocumentService {

    private DocumentDao documentDao;

    @Transational
    public Document getDocumentById(Long id) {
        return documentDao.getDocumentById(id);
    }

    //...
 
    @Transactional
    public void saveDocument(Document document) {
        documentDao.save(document);
    }
}

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

Но важно даже не это, в конце концов человек ко всему привыкает. Главное то, что выполнять их не зачем, прозрачной смены подсистемы хранения паттерн DAO на самом деле не предоставляет. Каждый рекламируемый вариант замены реализации DAO (переключение между ORM и JDBC, между реляционными СУБД и NoSQL, между СУБД и веб-сервисами и т.д.) имеет свои подводные камни и практически всегда требует внесения изменений в слой бизнес-логики.

1. Переключение между ORM и JDBC. ORM-фреймворки, в отличие от JDBC реализуют прозрачное хранение (т.н. transparent persistence). Если после загрузки объекта в сессию обратиться к его сеттерам, то после синхронизации сессии с базой данных эти изменения будут отражены в ней. При использовании же JDBC такой прозрачности нет, поэтому любые изменения необходимо явно синхронизировать с базой данных посредством вызова соответствующих методов DAO, а значит требуется добавить обращение к этим методами в слой бизнес-логики, т.к. ранее, при использовании ORM, они отсутствовали.

2. Переключение с традиционной реляционной СУБД на NoSQL-хранилище тоже не всегда можно сделать прозрачно. Большинство NoSQL-хранилищ не поддерживают транзакции в терминах ACID. Бизнес-логику, реализуемую в сервисах, придется адаптировать к нетранзакционному хранилищу. Например, для повышения быстродействия мы могли исключить некоторые проверки из бизнес-логики, надеясь, что в СУБД отработают ограничения целостности, а в случае их нарушения будет выброшено исключение и произойдет откат транзакции. Если же транзакции нет, то изменения, выполненные до момента выбрасывания исключения, сохранятся в БД, что приведет к нарушению целостности данных.

3. Аналогичные соображения можно высказать и относительно перехода с использования реляционной СУБД на взаимодействие с внешним хранилищем посредством веб-сервисов. Контекст транзакции по веб-сервисам без использования специальных техник, таких как WS-AtomicTransaction, не передается, а применение данных техник не всегда возможно, а там где возможно ведет к существенному снижению производительности.

4. Общее соображение. Давайте подумаем, что делать, если необходимо заменить слой хранения в сервисе частично. Предположим, что часть справочников будет подгружаться из системы управления мастер-данными (МДМ), а часть по прежнему храниться в СУБД. Нужно ли создавать композитные DAO-классы, в которых часть методов оперирует с МДМ, а остальные - с СУБД (т.е. по сути, произвести не расширение системы классов, а модификацию конкретного класса, тем самым нарушив принцип открытости-закрытости SOLID), или лучше инжектировать в сервис две реализации одного и того же DAO, затем три и т.д.?

Выводы

Радикальная смена подсистемы хранения только посредством использования паттерна DAO - миф. Как правило такая замена повлечет за собой и модификацию бизнес-логики. Максимум что можно сделать посредством данного паттерна - обеспечить переход на другую реализацию в рамках одной технологии. Например, сменить производителя и, соответственно, используемый диалект языка SQL в рамках технологии "реляционные СУБД". Т.е. перейти с Oracle на DB2 при использовании JDBC посредством паттерна DAO можно, а замена Oracle на Cassandra скорее всего потребует изменений и в слое бизнес-логики. Но заменить диалект СУБД при использовании ORM можно посредством изменения одной строчки в конфигурации. Более радикальные изменения потребуют модификации бизнес-логики и в случае использования структуры из "интерфейс сервиса - тело сервиса - интерфейс DAO - одна или несколько реализаций DAO", и в случае простой инъекции Entity Manager'а в сервис.

UPD: Статья вызвала некий резонанс. В обсуждении на форуме JavaTalks есть интересные мысли, но, как впрочем и ожидалось, дискуссия в конце концов свелась к переходу на личности и мнениям, что кто DAO не пишет, тому стоит перейти на другой язык программирования. Ситуация характерная для русскоязычной аудитории. Не служил - не мужик, че. Однако и в комментариях к данной заметке, и на форуме был высказан ряд соображений в пользу данного паттерна, достойных внимания.

  1. В приложении присутствует сложная логика доступа к данным и мы хотим эту логику тестировать. При этом хорошо, если у нас приложение переносимое между СУБД, тогда можем запускать тесты на какой-нибудь H2, если же приложение не переносимо, то придется запускать целевую СУБД, что во-первых, может быть медленне, а во-вторых, требует существенного усложнения окружения разработчика. В качестве контраргумента можно высказать следующее: если запросы инкапсулированы в одном месте приложения, например в мэпинге myBatis или вынесены в именованные запросы JPA, то их можно протестировать и без отдельного слоя DAO. Он будет нужен, если логика доступа к данным, не бизнес-логика, несколько сложнее, чем выполнение одного запроса на один метод, например, реализован поиск по нескольким источникам данных или шардинг, т.е. когда в банальном getById выполняется несколько запросов. Тогда в DAO действительно есть смысл.

  2. DAO облегчает модульное тестирование не слоя доступа к данным, а вышележащего слоя бизнес-логики. Можно сделать моки на объекты DAO и тестировать только логику. Если в проекте действительно используются модульные тесты бизнес-логики, то подход имеет право на жизнь.

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

  4. Пример из моей практики. Интеграционное приложение, в котором логика одна: перелей из пустого в порожнее и залогируй, а типов данных много. Тогда действительно разумно сделать сервис с одним методом, принимающий на вход два объекта-DAO: один отвечает за извлечение данных данного типа из одной информационной системы, другой - за их сохранение в соответствующей информационной системе. Здесь мы получаем все преимущества, приписываемые паттерну DAO: и переносимость, только не между источниками данных, а между информационными потоками, и полиморфизм, когда несколько классов, каждый из которых работает со своим типом данных, реализуют обобщенный интерфейс, а сервис, инкапсулирующий в себе бизнес-логику, работает только с этим интерфейсом. Сами DAO я немного описывал в своих предыдущих заметках (раз и два). Но такой подход весьма нишевый, далеко не в каждом приложении одна логика выпролняется над несколькими типами данных.

UPD 13.11.2014: Сейчас на практике получаю опыт миграции приложения с одной реляционной СУБД на другую и одной и той же СУБД между аппаратно/программными платформами. Надо сказать, что поведение СУБД зачастую бывавет очень разным. Дилемму "версионник-блокировочник" знают все, но в данном случае различия тоньше. При этом использование даже такого развитого ORM, как Hibernate, неспособно помочь. Приходится использовать нативные запросы и привлекать опытных DBA. Это я пишу к тому, что даже если вы протестировали ваше DAO на какой-нибудь In-Memory СУБД, то далеко не факт, что на промышленной системе код будет работать. Но тесты будут зелеными, некоторым это греет душу, да.

Понравилось сообщение - подпишитесь на блог

5 комментариев:

JMaestro комментирует...

Есть один плюс в использовании паттерна DAO не описанный в статье. Представьте что нужно реализовать бизнес логику по обработке данных, известно какая база данных будет использоваться и какая ее природа. Но реализации методов позволяющих доставать и сохранять данные еще нет. При использовании паттерна DAO мы можем эти методы заменить заглушками и уже в ближайшее время приступить к реализации бизнес логики до того как будем знать как эти данные хранятся

Pavel Samolisov комментирует...

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

Но даже так. Мы на каком-то этапе не знаем как будем хранить данные. Проектируем логику, что-то уже можно показать заказчику, ну т.е. оно будет жить пока сервер не перезагрузим. Но затем нюансы хранения утрясутся, методы реальной реализации мы напишем. И после этого нам придется всю жизнь поддерживать связку DAO-сервис только потому что когда-то мы не знали как будем хранить данные.

Впрочем, подозреваю, что возможен компромисс. Пока мы не знаем нюансов хранения - используем DAO, затем на каком-то этапе избавляемся от него.

Andrey Ershov комментирует...

Имхо основной плюс паттерна DAO - это гораздо более низкая стоимость поддержки и развития проекта в части работы с данными из БД.
Если команда применяет паттерн DAO, то вся работа с БД в проекте более организована и поянтна.
Диагностика ошибок - проще. Testability - выше. Можно написать отдельный тест для DAO метода, можно написать отдельный тест для Сервисного метода.
Если же доступ к данным производится напрямую из Сервисов через hibernateTemplate, например, то код сервисных методов иногда заметно сложнеет и главное, ошибки связанные с БД перемешиваются с ошибками в логике сервисов, и находить эти ошибки уже заметно сложнее чем в случае с использованием DAO.

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

Но сейчас есть spring-data: можно описывать DAO интерфейс, spring сам генерирует имплементацию в рантайме.
Получаем плюсы, которые я описал, и избавляемся сомнений.

Pavel Samolisov комментирует...

Возвращаясь к обсуждению ДАО и тестирования:

Мне интересно как могут выглядеть модульные тесты слоя ДАО. Модульный тест он же вроде как по-определению не должен соединяться с БД и что-то там делать. И что в результате тестируется? Что метод ДАО генерирует определенный запрос? А толку? Кто сказал, что этот запрос правильный и эффективный? Или коллеги делают все по максимуму: берут реальную СУБД, под которой предполагается эксплуатация, создают там тестовые данные, затем выполняют метод ДАО и смотрят, что же он нам вернул? Кто-то из комментаторов реально это делает или где-то завалялся мок Oracle?

Pavel Samolisov комментирует...

Добавил в заметку порцию выводов после обсуждения на JavaTalks. К сожалению тему на форуме закрыли. Если кто-то желает ответить, прошу сюда, в комментарии.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!