Вступление
Корпоративное приложение - это программное решение, созданное для нужд организации. Часто это крупномасштабная многоуровневая масштабируемая система. Корпоративное программное обеспечение может работать с большим количеством сложных данных, и для этого типа программного обеспечения важно иметь хорошую архитектуру.
Шаблоны архитектуры корпоративных приложений - это стандартизированные решения общих проблем, обнаруживаемых в больших системах. Они развивают архитектурное мышление и помогают разработчикам быть более уверенными в создании систем с проверенной надежностью.
Корпоративным приложениям можно поручить управление, отображение или хранение огромных объемов данных. При работе с этими приложениями не следует забывать об избежании тесной связи и обеспечении целостности / безопасности данных.
Объект передачи данных
Шаблон проектирования объекта передачи данных - это один из шаблонов архитектуры корпоративного приложения, который требует использования объектов, которые объединяют и инкапсулируют данные для передачи. Объект передачи данных , по сути, похож на структуру данных. Он не должен содержать никакой бизнес-логики, но должен содержать механизмы сериализации и десериализации.
DTO могут содержать либо все данные из источника, либо частичные данные. Они также могут хранить данные из одного или нескольких источников. При реализации DTO становятся средством передачи данных между системами.
Мартин Фаулер описывает объект передачи данных в своей знаменитой книге « Шаблоны архитектуры корпоративных приложений» . Основная идея DTO - уменьшить количество дорогостоящих удаленных вызовов.
Мартин Фаулер также определяет объект ассемблера , используемый для преобразования данных между DTO и любыми объектами сущностей. В настоящее время для этой цели мы используем картографы.
Стоит отметить, что применение шаблона объекта передачи данных может стать анти-шаблоном в локальных системах. Он предназначен для использования в удаленных вызовах для повышения безопасности и ослабления связи. Применительно к локальным системам это просто переоценка простой функции.
Мотивация
Предположим, нам нужно разработать корпоративную систему для компании. В систему будет входить база данных с различной общей информацией о сотрудниках - зарплата, проекты, сертификаты, личные данные (адрес, семейное положение, номер телефона и т. Д.).
Безопасность на входе в компанию требует доступа к нашей системе, чтобы идентифицировать работника, который хочет войти. Им нужна некоторая элементарная информация, такая как фамилия и фото работника.
Мы не хотим отправлять в систему безопасности другую конфиденциальную информацию, например личную информацию. Он избыточен и подвергает атакам канал связи между системами. Мы предоставим только то, что необходимо, а объем данных будет определен в DTO.
В приложениях Java - мы используем классы сущностей для представления таблиц в реляционной базе данных. Без DTO нам пришлось бы предоставлять все сущности удаленному интерфейсу. Это вызывает сильную связь между API и моделью сохранения.
Используя DTO для передачи только необходимой информации, мы ослабляем связь между API и нашей моделью, что позволяет нам более легко поддерживать и масштабировать сервис.
Реализация объекта передачи данных
Давайте сделаем приложение, которое позаботится об отслеживании местоположения ваших друзей. Мы создадим приложение Spring Boot, которое предоставляет REST API. Используя его, мы сможем получать местоположения пользователей из базы данных H2.
Если вы хотите прочитать об интеграции базы данных H2 с Spring Boot , мы вам поможем!
Настройка Spring Boot
Самый простой способ начать с пустого приложения Spring Boot - использовать Spring Initializr :
{.ezlazyload}
В качестве альтернативы вы также можете использовать Spring Boot CLI для начальной загрузки приложения:
$ spring init --dependencies=h2 data-transfer-object-demo
Если у вас уже есть приложение Maven / Spring, добавьте зависимость в
свой файл pom.xml
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${version}</version>
</dependency>
Или, если вы используете Gradle:
compile group: 'com.h2database', name: 'h2', version: '${version}'
Демо-приложение
Начнем с модели User
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String firstName;
private String lastName;
private String password;
private String email;
@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "location_id")
private Location location;
// Getters and Setters
}
Она содержит некоторую элементарную информацию , как username
,
firstName
, email
и т.д. Он также имеет отношение многие-к-одному с
Location
объекта:
@Entity
public class Location {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private double lat;
private double lng;
private String place;
private String description;
// Getters and Setters
}
Для основных операций CRUD мы будем полагаться на надежный
CrudRepository
предоставляемый Spring Boot:
@Repository
public interface UserRepository extends CrudRepository<User, Long>{}
@Repository
public interface LocationRepository extends CrudRepository<Location, Long> {}
Если вы не знаете, как это работает, мы рекомендуем прочитать наше Руководство по Spring Data JPA . Короче говоря, они предоставят нам базовую функциональность CRUD для наших моделей.
На этом этапе мы хотели бы создать контроллер, который обрабатывает
GET- запрос и возвращает список местоположений пользователя. Однако,
если мы извлекаем User
и Location
из нашей базы данных и просто
распечатываем необходимую информацию - другая информация, такая как
пароль, также будет содержаться в этом объекте. Мы не будем его
печатать, но он будет.
Давайте создадим объект передачи данных для передачи только
необходимой информации. И пока мы это делаем, давайте объединим
информацию о User
и Location
, чтобы данные передавались вместе:
public class UserLocationDTO {
private Long userId;
private String username;
private double lat;
private double lng;
private String place;
// Getters and Setters
}
Теперь этот объект содержит всю информацию, которую мы хотим показать
конечному пользователю. Теперь нам понадобится способ сопоставить
User
и Location
в один объект UserLocationDTO
Обычно это делается
с помощью инструментов сопоставления, таких как MapStruct или
ModelMapper , которые мы рассмотрим в последних разделах.
А пока выполним преобразование вручную. Поскольку нам понадобится
служба, которая вызывает наш UserRepository
, мы также сопоставляем
результаты и возвращаем DTO:
@Service
public class MapService {
@Autowired
private UserRepository userRepository;
public List<UserLocationDTO> getAllUsersLocation() {
return ((List<User>) userRepository
.findAll())
.stream()
.map(this::convertToUserLocationDTO)
.collect(Collectors.toList());
}
private UserLocationDTO convertToUserLocationDTO(User user) {
UserLocationDTO userLocationDTO = new UserLocationDTO();
userLocationDTO.setUserId(user.getId());
userLocationDTO.setUsername(user.getUsername());
Location location = user.getLocation();
userLocationDTO.setLat(location.getLat());
userLocationDTO.setLng(location.getLng());
userLocationDTO.setPlace(location.getPlace());
return userLocationDTO;
}
Получив список User
, мы напрямую конвертируем их вместе с их
информацией о Location
в объекты UserLocationDTO
При вызове этой
службы мы получим этот список DTO.
Наконец, давайте /map
чтобы кто-нибудь мог получить местоположение
пользователей:
@RestController
public class MapController {
@Autowired
private MapService mapService;
@GetMapping("/map")
@ResponseBody
public List<UserLocationDTO> getAllUsersLocation() {
List <UserLocationDTO> usersLocation = mapService.getAllUsersLocation();
return usersLocation;
}
}
Эта конечная точка просто возвращает @ResponseBody
. Он может быть
вызван пользователем или другой службой, которая анализирует результаты.
Давайте загрузим нашу базу данных фиктивной информацией для целей тестирования:
insert into location(id, lat, lng, place, description) values (1, 49.8, 24.03 ,'Lviv', 'Lviv is one of the largest and the most beautiful cities of Ukraine.');
insert into user(id, username, first_name, last_name, password, location_id) values (1, 'Romeo', 'Romeo', 'Montagues' ,'gjt6lf2nt5os', 1);
insert into user(id, username, first_name, last_name, password, location_id) values (2, 'Juliet', 'Juliet', 'Capulets' ,'s894mjg03hd0', 1);
Теперь, чтобы протестировать нашу конечную точку, мы будем использовать такой инструмент, как Postman, для достижения наших конечных точек:
{.ezlazyload}
Большой! Список наших пользователей возвращается только с необходимой информацией, как переданной, так и отображаемой.
Мы написали в нашей MapService
который объединяет и преобразует
данные, однако этот процесс можно легко автоматизировать.
Отображение с помощью ModelMapper
ModelMapper - отличная библиотека сопоставления, которая позволяет нам сопоставлять модели и DTO. Это упрощает отображение объектов, автоматически определяя, как одна объектная модель отображается на другую.
Чтобы добавить его в проект Maven, мы бы добавили зависимость:
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>${version}</version>
</dependency>
Или, если вы используете Gradle:
compile group: 'org.modelmapper', name: 'modelmapper', version: '${version}'
Давайте обновим наш предыдущий пример библиотекой ModelMapper:
@Service
public class MapService {
@Autowired
private UserRepository userRepository;
@Autowired
private ModelMapper modelMapper;
public List<UserLocationDTO> getAllUsersLocation() {
return ((List<User>) userRepository
.findAll())
.stream()
.map(this::convertToUserLocationDTO)
.collect(Collectors.toList());
}
private UserLocationDTO convertToUserLocationDTO(User user) {
modelMapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.LOOSE);
UserLocationDTO userLocationDTO = modelMapper
.map(user, UserLocationDTO.class);
return userLocationDTO;
}
}
Теперь вместо всего процесса назначения, который нам приходилось делать
раньше, мы просто map()
user
в UserLocationDTO
. Этот метод
сглаживает свойства User
в UserLocationDTO
при этом будут
присутствовать как информация о пользователе, так и его местоположение.
Примечание. При работе с объектами как с свойствами, например, с
нашим Location
является свойством User
, стандартный сопоставитель
библиотеки может не соответствовать всем свойствам. Мы установили
стратегию сопоставления на LOOSE
чтобы библиотеке было проще находить
и сопоставлять свойства.
Картографирование с помощью MapStruct
MapStruct - это генератор кода на основе Java с открытым исходным кодом, который создает код для реализации сопоставления.
Он использует обработку аннотаций для создания реализаций классов сопоставления во время компиляции и значительно сокращает объем стандартного кода, который обычно должен быть написан вручную.
Если вы используете Maven, установите MapStruct, добавив зависимость:
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
Эта зависимость импортирует основные аннотации MapStruct. Поскольку
MapStruct работает во время компиляции и прикреплен к сборщикам, таким
как Maven и Gradle, нам также придется добавить плагин в <build>
:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Если вы используете Gradle , установить MapStruct так же просто:
plugins {
id 'net.ltgt.apt' version '0.20'
}
// Depending on your IDE
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'
dependencies {
compile "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}
У нас User
и Location
, поэтому давайте создадим для них маппер:
@Mapper
public interface UserLocationMapper {
UserLocationMapper INSTANCE = Mappers.getMapper(UserLocationMapper.class);
@Mapping(source = "user.id", target = "userId")
UserLocationDTO toDto(User user, Location location);
}
Когда вы создаете проект, MapStruct подберет этот @Mapper
и
сгенерирует UserLocationMapperImpl
с полнофункциональной реализацией.
MapStruct имеет широкий спектр функций и расширенный набор функций. Если вам интересно узнать об этом больше, мы настоятельно рекомендуем прочитать наше подробное руководство по MapStruct на Java .
Заключение
В этой статье мы рассмотрели шаблон проектирования объекта передачи данных с его плюсами и минусами. Этот шаблон действительно предназначен только для удаленных вызовов, потому что преобразование из и в DTO может быть дорогостоящим.
Кроме того, мы создали демонстрационное приложение Spring Boot и исследовали два популярных средства сопоставления, которые можно использовать для упрощения процесса сопоставления между моделями и DTO.
Вы можете найти весь код проекта на GitHub .