Video thumbnail

    Андрей Беляев — DTO: живи быстро, гори ярко

    Valuable insights

    1.DTO уменьшают количество сетевых вызовов: Основная цель DTO — агрегировать необходимые данные в один объект для минимизации сетевых обращений между слоями приложения или клиентом и сервером.

    2.Слой DTO переживает приложение и базу данных: Поскольку DTO являются частью публичного API, они могут существовать дольше самого приложения или даже схемы базы данных, диктуя требования к слою доступа к данным.

    3.Выбор DTO определяет архитектуру слоя данных: Сложность и структура DTO напрямую влияют на то, как необходимо выбирать данные из базы, что может привести к неэффективным запросам или необходимости использовать проекции.

    4.MapStruct и ModelMapper — основные инструменты маппинга: Для автоматизации рутинного переноса данных используются библиотеки, такие как MapStruct, основанный на генерации кода во время компиляции, и ModelMapper, использующий рефлексию в рантайме.

    5.MapStruct проще отлаживать, чем ModelMapper: Код, сгенерированный MapStruct, виден разработчику, что упрощает отладку выражений. ModelMapper, работая через рефлексию, может скрывать ошибки в конвертерах до момента выполнения.

    6.Проекции Spring Data решают проблему выбора полей: Использование интерфейсов-проекций в Spring Data позволяет явно указать, какие поля необходимо выбрать из базы, обеспечивая типобезопасность и эффективность запросов.

    7.Сериализация DTO требует внимания к N+1 запросам: При сериализации сущностей, особенно с ленивой загрузкой, существует риск возникновения большого количества дополнительных запросов, если выборка данных не была предварительно настроена.

    Введение и определение DTO

    Обсуждаемая тема DTO является, казалось бы, банальной, но требует упорядочивания существующих знаний. Согласно определению Мартина Фаулера, DTO были придуманы для уменьшения количества сетевых вызовов между различными слоями приложения или между клиентской и серверной частями. Вместо выполнения десятка сетевых вызовов, все необходимые данные упаковываются в один объект и отправляются. Таким образом, DTO — это объект, который несет данные между слоями, например, от клиента к бизнес-логике или от слоя доступа к данным.

    Двойственность роли DTO

    В современном мире разработки приложений DTO описывают то, что требуется клиенту. Все чаще встречается ситуация, когда DTO определяются из описания API, а уже под них строится слой данных. Существует двойственность: с одной стороны, DTO — это нечто, что создается, реализуется, передает данные и завершает свою жизнь. С другой стороны, слой DTO живет долго, поскольку является частью API. Существует вероятность, что слой DTO переживет даже само приложение или изменение схемы базы данных, так как клиенты, взаимодействующие с API, могут быть долгоживущими.

    Типы DTO и их характеристики

    Опираясь на постулаты Адама Бина, рассматриваются DTO как носители данных. Они могут быть реализованы в виде классов, записей (records), массивов или отображений (maps). При проектировании DTO желательно, чтобы они обладали свойством неизменяемости (immutable) и были сериализуемыми. Важный момент проектирования структуры DTO — избегать циклических ссылок, поскольку библиотеки, работающие с DTO, могут некорректно обрабатывать такие структуры.

    Формы представления DTO

    Исторически первой формой были сущности, отдаваемые напрямую в сеть. Минус этого подхода — нарушение инкапсуляции: при изменении структуры данных ломается все, к чему привыкли клиенты, что требует версионирования. Этот подход хорошо работает только при наличии одного клиента, точно знающего, как отображать данные, например, в старых приложениях с шаблонизаторами вроде JSP.

    • Нарушение инкапсуляции при изменении структуры данных.
    • Необходимость введения дополнительного слоя версионирования.
    • Сложности при работе с коллекциями, которые могут быть лениво загружены.

    Эволюция DTO: Records и Maps

    В новых версиях Java появились Records, которые являются практически идеальной заменой классов DTO, так как они обеспечивают меньшее количество шаблонного кода (boilerplate) и не поддерживают наследование, что помогает избежать запутанных иерархий. Последним по предпочтению, но не по использованию, являются мапы. Мапы хороши тем, что работают везде и позволяют уложить в них что угодно без необходимости придумывать иерархии классов. Однако они не несут описания данных, требуя внешних описателей структуры.

    По предпочтениям, по умолчанию следует выбирать рекорды, а потом, если что-то идет не так, добавлять поджиги (DTO-классы).

    Жизненный цикл и проблемы маппинга

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

    Варианты реализации маппинга

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

    • Ручная реализация (самый простой, но требующий постоянной поддержки).
    • Собственный фреймворк с аннотациями или файлами конфигурации.
    • Использование специализированных библиотек (мапперов).

    При начале работы с DTO и ручном наполнении следует дважды подумать перед созданием собственного фреймворка, поскольку готовые мапперы реализуют те же принципы, но с меньшими затратами на поддержку.

    Сравнение библиотек для маппинга: MapStruct и ModelMapper

    Рассматриваются два семейства мапперов: MapStruct, использующий процессор аннотаций для генерации кода во время компиляции, и ModelMapper, который использует рефлексию и эвристики для маппинга в рантайме. MapStruct требует определения интерфейсов-мапперов, которые затем генерируют код, разбирающий иерархию типов.

    Характеристика
    MapStruct
    ModelMapper
    Механизм работы
    Генерация кода во время компиляции
    Рефлексия во время выполнения (Runtime)
    Поддержка Records
    Да (с оговорками)
    Нет (на момент 2020 года)
    Отладка
    Проще, код виден
    Сложнее, ошибки в рантайме

    Краевые ситуации: Вычисляемые свойства

    При необходимости создания вычисляемых свойств, например, конкатенации нескольких полей, MapStruct позволяет писать выражения на языке Java непосредственно в интерфейсе маппера. Минус этого подхода в том, что выражение не является типобезопасным до компиляции имплементации. ModelMapper использует собственный язык выражений (Specific Language) для простых операций, но для сложных вычислений требует написания кастомных конвертеров, что усложняет конфигурацию и отладку.

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

    Выборка данных и эффективность запросов

    Проблема выбора данных для наполнения DTO связана с тем, что при маппинге на сложные DTO не всегда известно, какие данные будут нужны. При использовании JPA это может привести к N+1 запросам или возникновению `LazyInitializationException`, если транзакция не продлена до момента маппинга в контроллере.

    Использование проекций Spring Data

    Более типобезопасным подходом является объявление интерфейса, описывающего только те поля, которые необходимы для DTO. Spring Data может пронюхать эти интерфейсы и выбрать только нужные поля, делая запрос эффективнее. В простых случаях Spring выбирает конкретные поля, но в сложных иерархиях проекций может возникнуть проблема, когда выбираются все поля.

    • Плюс: Эффективность запросов в простых случаях.
    • Минус: В сложных иерархиях Spring может выбирать все поля (N+1 проблема).
    • Плюс: Возможность использовать динамические методы репозитория для возврата разных типов проекций.

    Динамическое использование проекций в репозиториях

    Spring Data позволяет создать один метод репозитория, который динамически возвращает проекцию нужного типа, если передать ему нужную сигнатуру. Это позволяет избежать комбинаторного взрыва методов, но требует тщательной настройки или использования графов (например, с помощью JPA Graph) для решения проблемы N+1 в сложных случаях.

    Сериализация и валидация DTO

    Процесс десериализации в целом проще, чем сериализации, поскольку то, что можно было сериализовать, с высокой вероятностью можно и десериализовать. Основной вопрос при сериализации — сокрытие полей, которые не должны быть видны клиенту. Это можно реализовать, формируя DTO не на этапе реализации, а непосредственно перед отправкой ответа.

    Методы сокрытия полей

    Для сокрытия полей можно использовать разметку классов аннотациями (например, для XML) или использовать так называемые Jackson Views. View позволяет варьировать набор атрибутов, который будет отдан. Для этого сущность размечается нужными классами-идентификаторами View, и в эндпоинте указывается, какой сериализатор использовать.

    При сериализации сущностей на конечном этапе, если не предусмотреть выборку нужных данных, можно словить N+1 запрос.

    Новые подходы: JSON in DB

    В качестве нового подхода упоминается возможность хранения JSON непосредственно в реляционных таблицах (например, с использованием JSON-типов в базе данных). Это позволяет сконструировать View на основе этих таблиц и сразу получать структурированный JSON. Этот подход очень крут, так как данные уже подготовлены и сериализованы в базе, но он привязывает к структуре данных и усложняет версионирование API.

    Заключение и влияние на архитектуру

    Несмотря на кажущуюся простоту, DTO описывают API приложения и выполняют функцию передачи информации, а также декуплинга (разделения) слоев приложения. Слои DTO могут пережить само приложение, и их структура напрямую влияет на архитектуру слоя доступа к данным, особенно в части эффективности выборки.

    • Собственный фреймворк нужен только для уникального и сложного маппинга, который вы готовы поддерживать.
    • При использовании мапперов необходимо помнить, что выборка данных сильно зависит от структуры DTO, влияя на DAL.
    • Для эффективности запросов следует использовать проекции или настраивать выборку данных на этапе доступа к данным.

    Questions

    Common questions and answers from the video to help you understand the content better.

    Почему слой DTO может пережить само приложение и даже схему базы данных?

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

    В чем ключевое отличие MapStruct от ModelMapper с точки зрения времени выполнения?

    MapStruct выполняет генерацию кода маппинга во время компиляции, делая его более производительным и упрощая отладку. ModelMapper использует рефлексию для выполнения маппинга непосредственно во время выполнения (runtime).

    Как можно эффективно выбирать данные для DTO, избегая проблем с LazyInitializationException в JPA?

    Рекомендуется использовать проекции Spring Data, которые позволяют объявлять интерфейсы, выбирая только необходимые поля в SQL-запросе, или использовать инструменты вроде GraphQL для явного указания графа выбираемых сущностей.

    Нужно ли синхронизировать валидаторы, определенные на сущностях (entities) и на DTO?

    Этот вопрос остается открытым и зависит от усмотрения команды. Если DTO создаются из сущностей, валидаторы часто дублируются, но при изменении модели данных возникает проблема, как правильно синхронизировать эти валидаторы.

    Useful links

    These links were generated based on the content of the video to help you deepen your knowledge about the topics discussed.

    This article was AI generated. It may contain errors and should be verified with the original source.
    VideoToWordsClarifyTube

    © 2025 ClarifyTube. All rights reserved.