что такое dao и dto
Забудьте о DAO, используйте Repository
Недавно задумался о том, чем отличаются паттерны, позволяющие абстрагироваться от работы с хранилищем данных. Много раз поверхностно читал описания и различные реализации DAO и Repository, даже применял их в своих проектах, видимо, до конца не понимая концептуальных отличий. Решил разобраться, закопался в Google и нашел статью, которая для меня разъяснила все. Подумал, что неплохо было бы перевести ее на русский. Оригинал для англочитающих здесь. Остальным интересующимся добро пожаловать под кат.
Data Access Object (DAO) — широко распространенный паттерн для сохранения объектов бизнес-области в базе данных. В самом широком смысле, DAO — это класс, содержащий CRUD методы для конкретной сущности.
Предположим, что у нас имеется сущность Account, представленная следующим классом:
Создадим интерфейс DAO для данной сущности:
Паттерн Repository
Лучшим решением будет использование паттерна Repository. Эрик Эванс дал точное описание в своей книге: «Repository представляет собой все объекты определенного типа в виде концептуального множества. Его поведение похоже на поведение коллекции, за исключением более развитых возможностей для построения запросов».
Вернемся назад и спроектируем AccountRepository в соответствии с данным определением:
Методы add и update выглядят идентично методам AccountDAO. Метод remove отличается от метода удаления, определенного в DAO тем, что принимает Account в качестве параметра вместо userName (идентификатора аккаунта). Представление репозитория как коллекции меняет его восприятие. Вы избегаете раскрытия типа идентификатора аккаунта репозиторию. Это сделает вашу жизнь легче в том случае, если вы захотите использовать long для идентрификации аккаунтов.
Если вы задумываетесь о контрактах методов add/remove/update, просто подумайте об абстрации коллекции. Если вы задумаетесь о добавлении еще одного метода update для репозитория, подумайте, имеет ли смысл добавлять еще один метод update для коллекции.
Однако, метод query является особенным. Я бы не ожидал увидеть такой метод в классе коллекции. Что он делает?
Репозиторий отличается от коллекции, если рассматривать возможности для построения запросов. Имея коллекцию объектов в памяти, довольно просто перебрать все ее элементы и найти интересующий нас экземпляр. Репозиторий работает с большим набором объектов, чаще всего, находящихся вне оперативной памяти в момент выполнения запроса. Нецелесообразно загружать все аккаунты в память, если нам необходим один конкретный пользователь. Вместо этого, мы передаем репозиторию критерий, с помощью которого он сможет найти один или несколько объектов. Репозиторий может сгенерировать SQL запрос в том случае, если он использует базу данных в качестве бекэнда, или он может найти необходимый объект перебором, если используется коллекция в памяти.
Одна из часто используемых реализаций критерия — паттерн Specification (далее спецификация). Спецификация — это простой предикат, который принимает объект бизнес-области и возвращает boolean:
Итак, мы можем создавать реализации для каждого способа выполнения запросов к AccountRepository.
Обычная спецификация хорошо работает для репозитория в памяти, но не может быть использована с базой данных из-за неэффективности.
Для AccountRepository, работающего с SQL базой данных, спецификации необходимо реализовать интерфейс SqlSpecification:
Репозиторий, использующий базу данных в качестве бекэнда, может использовать данный интерфейс для получения параметров SQL запроса. Если бы в качестве бекэнда для репозитория использовался Hibernate, мы бы использовали интерфейс HibernateSpecification, который генерирует Criteria.
SQL- и Hibernate-репозитории не используется метод specified. Тем не менее, мы находим наличие реализации данного метода во всех классах преимуществом, т.к. таким образом мы сможем использовать заглушку для AccountRepository в тестовых целях а также в кеширующей реализации репозитория перед тем, как запрос будет направлен непосредственно к бекэнду.
Мы даже можем сделать еще один шаг и использовать композицию Spicification с ConjunctionSpecification и DisjunctionSpecification для выполнения более сложных запросов. Нам кажется, что данный вопрос выходит за рамки статьи. Заинтересованный читатель может найти подробности и примеры в книге Эванса.
В чем разница между DAL, DTO и DAO в трехуровневом архитектурном стиле, включая MVC
Недавно я изучал ORM (объектно-реляционное сопоставление) и трехуровневый стиль архитектуры (презентация, бизнес и сохранение данных ). Если я правильно понимаю, я могу разделить уровень сохранения данных на уровень DTO и DAO.
Я хотел бы понять, как следующие части работают вместе в слое сохранения данных.
Вдобавок я узнал, что
Буду рад, если кто-нибудь расскажет мне правду о том, как это работает вместе.
Пожалуйста, не закрывайте этот вопрос, потому что много разных выражений, я видел это везде, эти вещи связаны друг с другом в основном в больших приложениях, и я не могу представить, как это работает.
2 ответа
Объекты передачи данных. Обычно они используются для передачи данных от контроллера к клиенту (JS). Термин также используется для POCO / POJO теми немногими, которые фактически содержат данные, полученные из базы данных.
Уровень доступа к данным абстрагирует ваши действия с базой данных с помощью DAO / Repository / POCO и т. Д. ORM помогают вам создать DAL, но его также можно реализовать без их использования.
Как указано выше, модели потребляют большую часть вашей бизнес-логики. В N-уровневом приложении, если бизнес-логика полностью разделена с целью повторного использования между приложениями / платформами, то модели в MVC называются анемичными моделями. Если BI не нужно повторно использовать в этом масштабе в вашем приложении, вы можете использовать модель для его удержания. Никакой путаницы, правда?
Буду рад, если кто-нибудь расскажет мне правду о том, как это работает вместе.
Все шаблоны MV * определяют только идею / концепцию; они не определяют реализацию. Паттерны MV * в основном сосредоточены на отделении представления от BI. Просто сконцентрируйся на этом.
Обратитесь к этому ответу для получения подробной информации о различных объектах, содержащих данные.
Вы можете сначала провести различие между шаблоном MVC и трехуровневой архитектурой. Подводить итоги:
3-х уровневая архитектура:
Теперь для вышеупомянутой трехуровневой архитектуры шаблон MVC имеет место на уровне представления (для веб-приложения):
Жизненный цикл типичного HTTP-запроса:
36 уровень. Ответы на вопросы к собеседованию по теме уровня
MVC — это такой паттерн проектирования приложение, при котором приложение разделяется на три отдельные части: модель (model), представление (view) и контроллер (controller). Модель предоставляет данные и реагируется на команды контроллера, изменяя своё состояние. Представление отвечает за отображение данных модели пользователю, реагируя на изменения модели. А контроллер интерпретирует действия пользователя, оповещая модель о необходимости изменений. Таким образом каждый из компонентов этой схемы слабо связан с другими компонентами, за счёт чего достигается гибкость программы. Чаще всего вся бизнес-логика размещается в модели, хотя иногда она содержится и в контроллере. В первом случае модель называют тонкой, в последнем — толстой.
POJO переводится как «объект Java в старом стиле». Их противопоставляют EJB-объектами. Последние следуют специальной конвенции и обычно жёстко привязаны к какому-то конкретному enterprise-фреймворку (например, у них должен быть публичный конструктор без параметров, должен быть геттеры и сеттеры для полей, они должны быть сериализуемыми и т.д.). POJO — это, соответственно, обычный класс, не наследующий ни от каких специальных классов и не реализующий никаких специальных библиотек. Обычно POJO ничего особенного не делает, и содержит только состояние.
Entity Bean — это бин, цель которого хранить некоторые данные. В логику такого бина встроен механизм сохранения себя и своих полей в базу данных. Такой объект может быть уничтожен, а потом воссоздан из базы заново. Но кроме хранения данных у него нет никакой логики. А бин в свою очередь — это особый класс, которые должен выполнять следующие правила:
Очередь (Queue) — это структура данных, работающая по принципу «Первый вошёл — первый вышел». То есть элементы в очередь добавляются с одного конца, а извлекаются — с другого. Deque — это двусторонняя очередь. В этой очереди элементы можно добавлять как в начало, так и в конец, а также брать элементы тоже можно и из начала, и из конца очереди. Соответственно есть методы, которые позволяю положить элемент (это методы add(e) и offer(e)), и есть методы, позволяющие извлечь элемент из очереди (это такие методы, как remove() и poll()). Кроме того, есть методы, которые позволяют просто получить элемент из очереди без его удаления оттуда (это методы element() и peek()). В интерфейсе Deque дополнительно есть методы для добавления элементов в начало и конец очереди, извлечения элементов из начала или конца, а также получения элементов из начала или конца очереди (без их удаления из очереди).
Среди простых реализаций можно отметить ArrayDeque, LinkedList и PriorityQueue. Также существуют много классов в Concurrent Collections, которые реализуют эти два интерфейса (оба сразу или только один из них).
Переосмысление DTO в Java
Привет, Хабр! Представляю вашему вниманию любительский перевод статьи “Rethinking the Java DTO” Стивена Уотермана, где автор рассматривает интересный и нестандартный подход к использованию DTO в Java.
Я провел 12 недель в рамках программы подготовки выпускников Scott Logic, работая с другими выпускниками над внутренним проектом. И был момент, который застопорил меня больше других: структура и стиль написания наших DTO. Это вызывало массу споров и обсуждений на протяжении всего проекта, но в итоге я понял, что мне нравится использовать DTO.
Данный подход не является единственно верным решением, но он довольно интересный и отлично подходит для разработки с использованием современных IDE. Надеюсь, что изначальный шок пройдет и вам он тоже понравится.
Что такое DTO (Data Transfer Object)?
Зачастую, в клиент-серверных приложениях, данные на клиенте (слой представления) и на сервере (слой предметной области) структурируются по-разному. На стороне сервера это дает нам возможность комфортно хранить данные в базе данных или оптимизировать использование данных в угоду производительности, в то же время заниматься “user-friendly” отображением данных на клиенте, и, для серверной части, нужно найти способ как переводить данные из одного формата в другой. Конечно, существуют и другие архитектуры приложений, но мы остановимся на текущей в качестве упрощения. DTO-подобные объекты могут использоваться между любыми двумя слоями представления данных.
DTO — это так называемый value-object на стороне сервера, который хранит данные, используемые в слое представления. Мы разделим DTO на те, что мы используем при запросе (Request) и на те, что мы возвращаем в качестве ответа сервера (Response). В нашем случае, они автоматически сериализуются и десериализуются фреймворком Spring.
Представим, что у нас есть endpoint и DTO для запроса и ответа:
Что делают хорошие DTO?
Во-первых, очень важно понимать, что вы не обязаны использовать DTO. Это прежде всего паттерн и ваш код может работать отлично и без него.
Они также помогают документировать слой представления в человеко читаемом виде. Мне нравится использовать DTO и, я думаю, вы тоже могли бы их использовать, ведь это к тому же способствует уменьшению зацепления (decoupling) между слоем представления и предметным слоем, позволяя приложению быть более гибким и уменьшая сложность его дальнейшей разработки.
Тем не менее, не все DTO являются хорошими. Хорошие DTO помогают создавать API согласно лучшим практикам и в соответствии с принципам чистого кода.
Они должны позволять разработчикам писать API, которое внутренне согласовано. Описание параметра на одной из конечных точек (endpoint) должно применяться и к параметрам с тем же именем на всех связанных точках. В качестве примера, возьмём вышепредставленный фрагмент кода. Если поле price при запросе определено как “цена с НДС”, то и в ответе определение поля price не должно измениться. Согласованное API предотвращает ошибки, которые могли возникнуть из-за различий между конечными точками, и в то же время облегчает введение новых разработчиков в проект.
DTO должны быть надёжными и сводить к минимуму необходимость в написании шаблонного кода. Если при написании DTO легко допустить ошибку, то вам нужно прилагать дополнительные усилия, чтобы ваше API оставалось согласованным. DTO должны “легко читаться”, ведь даже если у нас есть хорошее описание данных из слоя представления — оно будет бесполезно, если его тяжело найти.
Давайте посмотрим на примеры DTO, а потом определим, соответствуют ли они нашим требованиям.
Покажи нам код!
Этот код на первый взгляд может показаться довольно странным, но не переживайте. В оставшейся части поста я объясню почему я остановился на данной реализации и какие преимущества она дает. Надеюсь, что вам станет всё понятно и вы оцените данный подход.
Он частично основывается на реальном коде из нашего проекта для выпускников, переведенный в контекст интернет-магазина. В нём каждый продукт имеет название, розничную и оптовую цену. Для хранения цены мы используем тип данных Double, но в реальных проектах вы должны использовать BigDecimal.
Мы создаем по одному файлу для каждого контроллера, который содержит базовый enum без значений, в нашем случае это ProductDTO. Внутри него, мы разделяем DTO на те, что относятся к запросам (Request) и на те, что относятся к ответу (Response). На каждый endpoint мы создаем по Request DTO и столько Response DTO сколько нам необходимо. В нашем случае у нас два Response DTO, где Public хранит данные для любого пользователя и Private который дополнительно содержит оптовую цену продукта.
Для каждого параметра мы создаем отдельный интерфейс с таким же именем. Каждый интерфейс содержит один-единственный метод — геттер для параметра, который он определяет. Любая валидация осуществляется через метод интерфейса. В нашем примере, аннотация @NotBlank проверяет что название продукта в DTO не содержит пустую строку.
Для каждого поля который входит в DTO мы реализовываем соответствующий интерфейс. В нашем случае аннотация @Value из библиотеки Lombok делает это за нас, автоматически генерируя геттеры.
Для полного сравнения, с использованием документации, вы можете посмотреть на примеры до и после. Также необходимо понимать, что это небольшие примеры и разница становится более наглядной как только вы начнете добавлять больше DTO.
“Это ужасно!”
Это на самом деле выглядит странно и здесь много необычных моментов. Давайте обсудим несколько из них подробнее.
Мы используем слишком много интерфейсов — по одному на каждый параметр! Мы делаем это потому что считаем данные интерфейсы единственным источником описательной информации относительно параметра который он определяет. Далее мы поговорим об этом чуть больше, но поверьте мне, это принесет свои плоды.
Мы не реализовали методы интерфейсов. Да, выглядит немного странно и я хотел бы найти решение получше. Сейчас мы используем автогенерацию геттеров при помощи Lombok для закрытия контракта и это небольшой хак. Выглядело бы лучше, если бы мы могли объявлять поля сразу в интерфейсе, что позволяло бы создавать DTO в одной строчке кода. Однако, в java нет возможности интерфейсам иметь не статические поля. Если вы будете использовать этот подход в других языках, то возможно ваш код будет более лаконичным.
Это (почти) идеально
Давайте вернемся к нашим требованиям к созданию хорошего DTO. Соотвествует ли им наш подход?
Согласованный синтаксис
Мы определенно улучшили согласованность синтаксиса и это главное почему мы могли бы начать использовать данный паттерн. Каждый API параметр теперь имеет свой синтаксис, определенный через интерфейс. Если DTO содержит опечатку в имени параметра или некорректный тип — код просто не скомпилируется и IDE выдаст вам ошибку. Для примера:
К тому же, когда мы используем валидацию на уровне интерфейса, мы исключаем ситуацию, когда один и тот же параметр на одном endpoint проходит валидацию и не проходит её на другом.
Согласованная семантика
Такой стиль написания DTO улучшает понимание кода через наследование документации. Каждый параметр имеет свою семантику которая определена в геттер методах соответствующего ему интерфейса. Пример:
Как только в DTO мы реализовали данный интерфейс, наша документация автоматически стала доступна через геттер.
Теперь вы гарантировано получаете актуальную и целостную документацию во всех DTO, которые реализовали данный интерфейс. В редких случаях, когда вам нужно добавить API, параметр с уже используемым наименованием и разной семантикой, вам придется создать отдельный интерфейс. Хоть это и неудобно, но заставляет разработчиков задуматься в таких ситуациях, а будущим читателям этого кода понять разницу между схожими параметрами.
Читабельность & Поддерживаемость
Будем честны: в нашем подходе достаточно много шаблонного кода. У нас есть 4 интерфейса, без которых не обойтись, и каждый DTO имеет длинную строку с перечислением интерфейсов. Мы можем вынести интерфейсы в отдельный пакет, что поможет избежать лишних “шумов” в коде c описанием DTO. Но даже после этого, бойлерплейт остается главным недостатком данного подхода, что может оказаться веской причиной для того чтобы использовать другой стиль. Для меня, эти затраты все еще стоят того.
К тому же, мы видим всю структуру наших DTO классов. Посмотрите на код и вы увидите все что вам нужно знать из сигнатуры класса. Каждое поле указано в списке реализованных интерфейсов. Достаточно нажать ctrl + q в IntelliJ и вы увидите список полей.
В нашем подходе мы пишем валидацию единоразово, т.к. она реализуется через методы интерфейса. Создали новое DTO — получили валидацию в подарок, после реализации интерфейса.
И в заключении, благодаря нашим интерфейсам, мы способны писать переиспользуемые утилитные методы. В качестве примера, рассмотрим ситуацию, когда нам нужно посчитать наценку на товар:
В java, мы можем реализовать это используя обобщение:
Вывод
Я не жду, что вы сразу же пойдете переписывать все ваши DTO. Но есть несколько деталей которые вы можете почерпнуть для себя:
Что такое dao и dto
Описать простыми словами термины, встречающиеся при разработке web-приложений:
из браузера
web (controller, view, ui) layer
service layer
dao (repository) layer
domain (model, entity) layer
Plain Old Java Object, старый добрый Java-объект — это объект, состоящий чаще всего из набора полей, их геттеров/сеттеров и без дополнительной нагрузки в виде:
Extend prespecified classes, as in
Implement prespecified interfaces, as in
Contain prespecified annotations, as in (хотя статья на сайте Спринга допускает наличие аннотаций для POJO-объектов )
Термин POJO возник в качестве ответной реакции на появление платформы J2EE и ее широко распространившееся внедрение в приложениях, из-за чего, в частности, усложнился весь процесс их разработки. Мартин Фаулер с коллегами придумали данный термин для описания класса, свободного от «немого» кода, который требовался лишь для корректной работы среды выполнения
POJO был представлен в качестве альтернативы для Enterprise JavaBeans (EJB) и других «тяжелых» enterprise-конструкций, которые были популярны в 2000-х годах (об этом можно почитать в статье Сергея Немчинского)
Пример:
Таким образом, основной посыл POJO — упрощение классов-сущностей насколько, насколько это возможно для моделирования предметной области
Резюмируя все вышесказанное, можно сказать, что POJO — этот класс, который ничего не делает и имеет только состояние
не путать с Enterprise JavaBeans
это класс в языке Java, написанный по определённым правилам:
Пример:
«Сущности» — это классы, моделирующие объекты предметной области
По своей структуре они приближены к таблицам базы данных (типы и названия столбцов таблицы == типам и названиям полей в классе, взаимодействующему с конкретной таблицей)
Хранятся в domain слое
Сущности почти всегда изменяемы (мутабельны)
«Объект-значение» ⎼ термин из среды DDD ⎼ это объект без специальных методов, имеющий набор свойств (полей) примитивных типов данных или тоже Value object
Объекты данного рода проверяются на равенство исходя не из физической одинаковости (одинаковости ссылок на них), а из значений свойств
Примером VO является любой класс, который реализует равенство через равенство содержащихся в нем данных
В отличие от Entity не обладает идентичностью. На практике это означает, что объекты-значения не имеют поля-идентификатора: если два экземпляра одного объекта-значения обладают одинаковым набором атрибутов, то они равны
Объекты-значения должны быть неизменяемы (immutable). Это значит, что если требуется изменить такой объект, то для этого придется создать его новый экземпляр, вместо того чтобы изменять существующий
Объект-значение всегда должен принадлежать одной или нескольким сущностям, он не может жить собственной жизнью
Чтобы распознать объект-значение, мысленно замените его на Integer
Объекты-значения не должны иметь собственной таблицы в базе данных
Data Transfer Object, объект переноса данных ⎼ это паттерн, который предполагает использование отдельных классов для передачи данных (объектов без поведения) между слоями, c целью уменьшения количества запросов к базе данных
DTO можно рассматривать как хранилище информации, единственная цель которого — передать данные получателю
Примером DTO является любой класс, который содержит только поля и методы по извлечению этих данных
Data Access Object, объект доступа к данным — абстрактный интерфейс к какому-либо типу базы данных или иному механизму хранения
Описание проблемы
Способ доступа к данным бывает разным и зависит от источника данных. Способ доступа к базе данных зависит от типа хранилища (реляционные базы данных, объектно-ориентированные базы данных, однородные или «плоские» файлы и т.д.). Унифицированный API для доступа к этим несовместимым системам отсутствует. А использование конкретного способа доступа создает зависимость между кодом приложения и кодом доступа к данным
Такая зависимость кода может сделать миграцию приложения от одного типа источника данных к другому трудной и громоздкой
Для доступа к данным хотелось бы использовать какой-либо универсальный способ (паттерн), позволяющий скрыть процесс их получения
Для решения вышеперечисленной проблемы обычно используют паттерн DAO
Описание паттерна
Данный паттерн абстрагирует и инкапсулирует доступ к источнику данных, а также управляет соединением с ним для получения и записи данных
В самом широком смысле, DAO — это класс, содержащий CRUD-методы для конкретной сущности
DAO полностью скрывает от клиента детали реализации взаимодействия с хранилищем данных. Поскольку при изменениях источника данных представляемый DAO интерфейс не изменяется, этот паттерн дает возможность DAO принимать различные схемы хранилищ без влияния на клиента или бизнес-компоненты. По существу, DAO выполняет функцию адаптера между компонентом и источником данных
Шаблон DAO используется для связи программы, написанной на Java с реляционными базами данных через интерфейс JDBC
JDBC API позволяет в приложениях использовать SQL-команды, являющиеся стандартным средством доступа к таблицам
Также DAO — это промежуточный слой, который скрывает от клиента реализацию взаимодействия с разными хранилищами данных, способы и механизмы хранения, предлагая при этом единые требования по функционалу в виде интерфейса
Паттерн DAO позволяет:
Проблемы DAO
Большая часть людей представляет DAO некими вратами к базам данных, беспрепятственно добавляя в него множество методов для доступа к базе. Поэтому нередко можно увидеть слишком раздутое DAO c большим количеством методов на все случаи жизни
Чтобы уйти от этого, предлагается использовать паттерн Repository
«Хранилище» ⎼ паттерн, выполняющий роль коллекции объектов из domain layer в оперативной памяти
Хранилище позволяет добавлять или удалять объекты, как будто мы работаем с обычными коллекциями
Тот факт, что объект из domain layer на самом деле не находятся в хранилище, полностью скрыт от клиентских программ. Для них ⎼ это выглядит, как коллекция в памяти
Данный слой используется в Spring Data JPA
REpresentational State Transfer, передача состояния представления
REST определяет набор операций, которые, например User может выполнять над Meals
Дополнительная информация: