В ходе разработки любого приложения программист сталкивается с необходимостью работать с моделями различных объектов, созданных для разных целей. И соответственно, с необходимостью конвертировать их между собой. Если ваш проект на начальном этапе развития, то можно, конечно, использовать рукописные конверторы. Но рано или поздно проект станет больше, и вы столкнётесь с необходимостью использовать уже готовое решение для конвертации моделей.
Одним из таких решений является МodelMapper. Его очень просто использовать в самом начале проекта без особых знаний. Попытаюсь разобрать основные моменты использования фреймворка.
Как начать использовать ModelMapper?
Сначала добавляем его в зависимости. Если вы используете maven, то:
Предположим, у нас простой проект с базой данных и RestAPI и нам необходимо превратить entity в dto и обратно. На этапе прототипа проекта могут полностью совпадать, и в таком простейшем примере нам вообще не нужно ничего дополнительного писать. МodelMapper всё сделает за нас.
В примере, представленном ниже, я буду использовать аннотации Lombok, чтобы было проще =)
МodelMapper по названию полей сам догадывается, что на что нужно маппить. Это очень удобно, если у вас есть множество моделек, которые в целом похожи друг на друга. Весь процесс можно разбить на две части: распознание и связь полей, а также перенос значений.
По умолчанию ModelMapper ищет поля, помеченные как public, и использует JavaBeans Naming Convention, чтобы определить какие проперти соответствуют друг другу. Каждый шаг распознавания поля и связи с полем модели назначения можно настроить;
AccessLevel — имеет следующие значения PUBLIC, PROTECTED, PACKAGE_PRIVATE, PRIVATE;
NamingConvention — имеет следующие значения JAVABEANS_ACCESSOR, JAVABEANS_MUTATOR, NONE JAVABEANS_ACCESSOR ищет гетторы а JAVABEANS_MUTATOR ищет сеттеры, так-же можно создать свой NamingConvention dspdfd NamingConvention.builder();
NameTokenizers — имеет следующие значения CAMEL_CASE, UNDERSCORE эта опция используется для глубокого маппинга, пример маппинга имени автора выше;
MatchingStrategies — может быть STANDARD, LOOSE, STRICT по умолчанию стоит STANDARD.
Если описать работу маппера простыми словами, то: он сканирует поля в соответствии с AccessLevel, парсит их и бьёт на токены, сравнивая эти токены он пытается понять подходит ли поле для маппинга. Стратегии настраивают степень точности:
STRICT — все токены должны быть в одном порядке, а также все токены модели источника должны совпадать с токенами модели получателя;
STANDARD — порядок токенов может не совпадать, все токены цели должны совпадать и только один токен источника должен совпадать.;
LOOSE — порядок токенов может не совпадать, только один токен модели источника и получателя должен совпадать.
Пример настройки значений для ModelMapper:
public BookConvertor() {
this.modelMapper = new ModelMapper();
Configuration configuration = modelMapper.getConfiguration();
configuration.setFieldAccessLevel(Configuration.AccessLevel.PUBLIC);
configuration.setSourceNamingConvention(NamingConventions.JAVABEANS_ACCESSOR);
configuration.setDestinationNamingConvention(NamingConventions.JAVABEANS_MUTATOR);
configuration.setSourceNameTokenizer(NameTokenizers.CAMEL_CASE);
configuration.setDestinationNameTokenizer(NameTokenizers.CAMEL_CASE);
configuration.setMatchingStrategy(MatchingStrategies.STANDARD);
}
Это далеко не все настройки МodelMapper, больше настроек можно посмотреть в классе InheritingConfiguration.
Маппинг отдельных полей
Для начинающего специалиста, показанного выше, вполне достаточно. Но для серьёзного приложения нужен больший контроль над маппингом определённых полей. Также было бы удобно маппить вложенные сущности. В этом разделе мы рассмотрим, как нам с этим поможет МodelMapper.
Давайте немного усложним наш маппинг. Предположим, что нам в нашем поле index не нужна подстрока ISBN:. Как нам изменить условия маппинга, чтобы для одного поля мы удаляли эту подстроку?
Можно использовать Converter<S,D> :
package org.example.convertor;
import org.example.dto.BookDto;
import org.example.entity.BookEntity;
import org.modelmapper.Converter;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Component;
@Component
public class BookConvertor {
private final ModelMapper modelMapper;
private Converter<String, String> isbnRemover = (src) -> src.getSource().replaceAll("ISBN: ", "");
public BookConvertor() {
this.modelMapper = new ModelMapper();
modelMapper.createTypeMap(BookEntity.class, BookDto.class)
.addMappings(mapper -> mapper.using(isbnRemover).map(BookEntity::getIndex, BookDto::setIndex));
}
public BookDto convertToDto(BookEntity entity) {
return modelMapper.map(entity, BookDto.class);
}
}
В данном примере мы создали TypeMap для двух наших объектов и указали поле, для которого мы хотим использовать этот конвертер.
Также конвертер можно добавить и для всего МodelMapper, если написать modelMapper.addConverter(isbnRemover); . Тогда он будет применён для всех конвертаций String->String.
А также можно создать конвертер на весь TypeMap: modelMapper.createTypeMap(BookEntity.class, BookDto.class).setConverter(ctx->BookDto.builder().build());, но тогда будет возвращён пустой объект BookDto.
Мы научились модифицировать правила конвертации отдельных полей и целых объектов. С этим уже можно полноценно работать. Но бывают случаи, когда нам не нужно модифицировать значение, а необходимо просто связать два названных по-разному поля.
Добавим в наши объекты поля: comment в BookEntity и review в BookDto и модифицируем наш BookConverter:
package org.example.convertor;
import org.example.dto.BookDto;
import org.example.entity.BookEntity;
import org.modelmapper.Converter;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Component;
@Component
public class BookConvertor {
private final ModelMapper modelMapper;
private Converter<String, String> isbnRemover = (src) -> src.getSource().replaceAll("ISBN: ", "");
public BookConvertor() {
this.modelMapper = new ModelMapper();
modelMapper.createTypeMap(BookEntity.class, BookDto.class)
.addMapping(BookEntity::getComment,BookDto::setReview)
.addMappings(mapper -> mapper.using(isbnRemover).map(BookEntity::getIndex, BookDto::setIndex));
}
public BookDto convertToDto(BookEntity entity) {
return modelMapper.map(entity, BookDto.class);
}
}
И тогда запрос будет выглядеть вот так:
[
{
"bookName": "Приключения Оливера Твиста",
"authorName": "Чарльз Диккенс",
"pages": 220,
"index": "978-5-91921-226-3",
"review": "Отличный приключенчиский роман"
},
{
"bookName": "Гордость и предубеждение",
"authorName": "Джейн Остин ",
"pages": 400,
"index": "978-5-699-52151-7",
"review": "Занудная история про богатеев в Америке"
},
{
"bookName": "Фауст",
"authorName": "Иоганн Вольфганг фон Гёте",
"pages": 270,
"index": "5-699-07346-9",
"review": "Пища для ума"
}
]
Теперь при маппинге отдельных полей у нас будет меньше мороки.
А что, если нам нужно маппить ещё и вложенную сущность? Для этого мы снова модифицируем BookDto и добавляем туда поле author вместо authorName. А также создаём класс AuthorDto, содержащий только поле name.
И наш BookConverter теперь будет выглядеть следующим образом:
Самое просто мы разобрали ранее. Теперь давайте посмотрим, как же ModelМapper работает с наследованием. Для этого мы изменим модель наших данных, добавив наследников для книг.
Для того, чтобы ModelМapper понял, что AudioBookEntity и HardCoverBookEntity — это наследники BookEntity, мы должны к TypeMap<BookEntity,BookDto> вызвать include и добавить маппинги. Но, к сожалению, для внутренних маппингов нам надо будет указывать вручную маппинг всех полей, как показано в примере. Эта особенность может стать проблемой если у вас на проекте примитивные базовые классы и развитая иерархия наследования классов.
ModelМapper — это удобный фреймворк, который можно использовать как на старте вашего проекта, так и на более поздних этапах. Но у него, как и у любого инструмента, есть свои слабые стороны и ограничения, о которых стоит знать и которые стоит учитывать.