Как стать автором
Обновить

Комментарии 35

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

Вот причины:

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

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

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

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

В-пятых, IDE при рефакторинге может не подхватить строковые литералы, символизирующие имена полей и методов. А вот если бы были тесты...

Но этот все не отменят пользы от такого рода мапперов, если перечисленные недостатки не имеют значения в силу специфики проекта.

  1. Тесты и так и так будут одинаковыми, ведь нужно протестировать всю логику маппинга) огромное преимущество - это null safe библиотека, как минимум все эти кейсы проверять не нужно

  2. Про сбой - спорно, но окэй, такое бывает, нужно смотреть что генерируется.

  3. В статье есть пример про вынос некоторой логики в отдельные классы, так же можно выносить общие маппинги в отдельные интерфейсы и тоже их использовать. Это очень удобно.

  4. В IntelliJ idea есть шикарный плагин для map struct’a , который подсвечивает и видит весь синтаксис, даже в строках в expression

  5. На самом деле map struct далеко не идеален, есть сложные вещи, например прикидывание в контексте нескольких параметров, но в статье про подводные камни не сказано(

Да, в MapStruct есть достаточное количество нюансов и ограничений. Цель статьи другая - сэкономить время тех, кто только знакомится с библиотекой.

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

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

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

По моему опыту, гораздо раньше и чаще наступает момент, когда разработчик в самописном маппере забывает прописать проверку на null или забывает добавить новое поле.

А вот с пониманием у mapStruct как раз все неплохо (в отличии от runtime-мапперов). Есть сгенеренный код, вполне читаемый.

Поэтому нужны тесты и Kotlin, чтобы проверять поля и не проверять null-ы.

Думаете, если разработчик забыл добавить поле в основной код, он не забудет добавить его в тест?

С mapstruct то же самое - если не протестил маппинг, то не факт что оно работает.

Может быть новое поле он и прокинет сам, а может и неправильно прокинет.

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

Один из больших минусов - код у него внутри на java. И когда используешь его с DTO на kotlin, он генерит код с супер-странной nullability, и в рантайме частенько можно ловить NPE, когда он пытается в not-null поле этот самый null положить.

Может быть новое поле он и прокинет сам, а может и неправильно прокинет

Прописывайте поля явно, через @Mapping. Тогда вероятность, что упадет при компиляции сильно выше

Один из больших минусов - код у него внутри на java. И когда используешь его с DTO на kotlin

Не видел, чтобы разработчики MapStruct заявляли о поддержке котлина. А если так, то претензия такая себе.

претензия такая себе.

Ну извините, что вам не понравилось, у меня вот такая)

Сейчас на бэке проекты пишут либо с Lombok, либо на Kotlin. Тот же spring поддерживает kotlin, не обламывается.

Они мне конечно ничем не обязаны. Но от этого более удобно мне не становится ни с какой стороны.

А от забывчивости разработчика увы средств особо нет, кроме как проверять всю сделанную им работу на код ревью и на приемочных тестах. И то не 100% гарантия. И mapstruct это никак не меняет в целом.

А от забывчивости разработчика увы средств особо нет

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

Тогда можно пойти чуть дальше и сказать, что может быть вообще все тесты не нужны. Зачем - ведь разработчик так и так что-то забудет, не в коде так в тесте.

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

Опять же, тесты хороши тем, что через них можно сделать регресс. Вот например, захотите вы обновить версию мапструкта с 3 на 4, как будете убеждаться что в вашем большом проекте всё ок после апдейта, если нет на это тестов?

Тогда можно пойти чуть дальше и сказать, что может быть вообще все тесты не нужны

Я этого не говорил, читайте внимательно. Там где нет логики. Если поле вычисляется на основе каких-то правил, то тест конечно же нужен.

Вот например, захотите вы обновить версию мапструкта с 3 на 4

Мапперы же не в вакууме работают. Если тесты на сервисе, где используется маппер начал падать, то вот вам и сигнал. Но вообще проверять совместимость версии должны авторы мапстракта и писать об этом в release notes, а вы, соответственно, прочитать и принять решение.

Я вот тоже хочу пойти "чуть дальше" и спросить, а вы пишите тесты на сериализатор в json? А то вдруг вы обновите версию какого-нить jackson, а у вас все сломается.

Если тесты на сервисе, где используется маппер начал падать, то вот вам и сигнал

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

а вы пишите тесты на сериализатор в json

В некотором смысле да, например если это rest-контроллер, то нужен тест на основе mockMvc, который проверит, какой именно json возвращает сервер. Вы может быть ожидаете, что это тестировать не нужно и там всё работает само и со 100% гарантией, но на практике это не так. Там стоит проверять например форматы даты-времени, или во что сериализуются какие-то нестандартные типы, типа BigDecimal

проверять совместимость версии должны авторы мапстракта

Вы так будете объяснять бизнесу, когда из-за бага в мапстракте баг будет уже у вас, и на проде будет инцидент? Сомневаюсь, что они оценят :)

Но у вас же нет теста именно сериализатора, как вы бизнесу будете объяснять, если что случится?) А ещё orm может неправильный sql сгенерить, давайте тоже тестами обмажем.

Короче, я считаю, что проверять отдельно маппер избыточно, если есть тест на сервис

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

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

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

Хотя на простых проектах, где нагрузки нет, и нет больших потерь денег из-за подобных ошибок, всё это в общем-то и не нужно.

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

Именно это я и хотел донести.

Хотя на простых проектах, где нагрузки нет, и нет больших потерь денег из-за подобных ошибок, всё это в общем-то и не нужно

Я бы сказал, что очень на редких проектах это оправдано. Но, конечно же, такие проекты есть. Кстати, вспомнил статью про Oracle, может интересно будет

Согласен по всем пунктам и даже без но.

Так же скажу, что это верно и для сериализаторов/десериализаторов.

Кто-то поменял username на user_name и отвалилась половина зависимых сервисов.

А тесты замоканы. Там всегда один и тот же json.

Тоже самое можно и про ORM сказать, если иметь достаточно компетенции можно заранее писать высокопроизводительные запросы в БД и поддерживать все в идеальном состоянии. Таким образом, исключая ряд проблем.

Но как мы знаем этого не происходит потому что это отнимает слишком много времени, один человек не справится с поддержкой 15+ сервисов. А у двух человек разные взгляды на разработку одного и того же продукта и далеко не факт, что вы договоритесь.

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


И лучше иметь одинаковые проблемы и соответственно пути их решения на 15+ сервисах, чем за год сделать ничего продумывая, как же все это идеально реализовать. Постоянно переписывая код до идеального состояния.

По сути Mapstruct и другие "мапперы" это способ ускорить разработку здесь и сейчас в обмен на возможные проблемы в будущем. "Здесь и сейчас" по сути это то, что крайне важно для бизнеса потому, как, если не успеешь за конкурентами, они тебя обгонят и съедят.

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

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

Вывод: важно реалистично подходить к разработке проекта, и там где необходимо использовать ручной подход, а там где это не критично использовать mapstruct или другие инструменты.

Для меня огромным преимуществом MapStruct стало то, что с его помощью можно формально проверить комплектность (все нужные свойства) мапинга. Представьте приложение, которое читает данные с RabbitMQ записывает их до Redis и раздаёт потом через REST (несколько версий).

RabbitMqObject -> ApplicationModel -> (RedisPersistance?) -> ApplicationModel -> REST1.0...REST5.0

8 уровней мапирования :) Если я использую MapStruct то пре правильной настройке появление/удаление/(частично изменение) атрибута в RabbitMqObject приведёт к ошибке компиляции RabbitMqObject -> ApplicationModel и я буду вынужден решать эту проблему сразу как она возникнет.

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

Работая в Сбере, видел как люди пишут тесты на эти маперы. По сути тестируют не свой функционал и бизнес логику, а чужую библиотеку Mapstruct. Это ради прохождения порога 80 покрытия тестов

Некоторые пакеты можно исключать из покрытия, что является классной практикой. У нас, например, сущности и мапперы исключены из подсчета покрытия. Это можно гибко настраивать в JacoCo или сонаре.

Это ради прохождения порога 80 покрытия тестов

Дурное дело - не хитрое. Но мапперы тут не при чем

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

Вообще жаль, что заповедь "Явное лучше неявного" находится в манифесте питона, а не джавы.

"Явное лучше неявного" находится в манифесте питона, а не джавы

Вы, наверное, не видели другие мапперы, где все мапится в runtime через reflection. Вот это да, неявное. А mapstruct генерит код, явный и понятный. Пошел и посмотрел, если есть вопросы

Вопрос не что он генерит, а как заставить его генерить так, как требуется в нетривиальных случаях. Даже в этой статье виден целый новый DSL для изучения: @Mapper, @Mapping, componentModel, uses, import, target, qualifiedByName. И спринг еще сбоку со своими бинами лезет. Уильям Оккам тяжело вздыхает в садах Эдема.

как требуется в нетривиальных случаях

Ну так нетривиальные случаи потому и нетривиальные, что требует дополнительных сил и знаний. Но если сложно или не хочется разобраться с mapStruct, то не обязательно его использовать для таких случаев. У него есть возможность вызывать для маппинга ваш метод. Напишите "явно" руками то, что нужно, и пусть mapStruct использует ваш метод.

Но для подавляющего большинства случаев ничего такого не требуется. И mapStruct при этом очень сильно экономит время и спасает от рутины.

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

В интерфейсах default методы тоже никто не отменял, отлично работает с мапстрактом

Опять же, для MapStruct есть отличные инструменты прямо в IDEA. Тот же Amplicode например позволяет генерить сами мапперы и методы маппинга. IDEA же отлично умеет их дебажить. Да и в целом наличие нормального тулинга для Spring делает все неявное вполне явным и не порождающим боль при использовании

в User установился пароль со значением defaultValue = "pass123"

Мне кажется, на скриншоте другое значение.

Спасибо за бдительность, поправил!

Теоретический вопрос: можно ли как-то в рантайме выяснить, что пришли лишние поля, о которых маппер не знает? Аналог растовского #[serde(deny_unknown_fields)].

Вы путаете с сериализатором/десериализатором (в Java мире это, например, Jackson). MapStruct конвертирует одну структуру в другую и генерирует код для этого на этапе компиляции (перед ней, если быть точным) - все поля известны уже тогда.

 defaultValue устанавливает указанное значение для всех полей, вне зависимости от условий

Здесь ошибка. defaultValue устанавливает значение, если source == null, а если нужно вне зависимости от условий - это constant.
Пример как использовать defaultValue из javaDoc'a https://mapstruct.org/documentation/1.6/api/org/mapstruct/Mapping.html#defaultValue()

 // We need map Human.name to HumanDto.fullName, but if Human.name == null, then set value "Somebody"
 // we can use defaultValue() or defaultExpression() for it
 @Mapper
 public interface HumanMapper {
    @Mapping(source="name", target="name", defaultValue="Somebody")
    HumanDto toHumanDto(Human human)
 }

Спасибо, поправил!

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории