JavaRush/Java блог/Java Developer/Spring — это не страшно, прослойка из DTO
Павел
11 уровень

Spring — это не страшно, прослойка из DTO

Статья из группы Java Developer
участников
СОДЕРЖАНИЕ ЦИКЛА СТАТЕЙ Продолжаем говорить про Spring. Сегодня будем разбирать паттерн DTO, для понимания можно почитать тут. Самое сложное в DTO - это понять, зачем оно нужно. Давайте займемся спекуляцией овощей, и заодно, попишем код, может по ходу дела что то и проясниться. Создайте spring-boot проект , подключите h2 и Lombok. Создайте пакеты: entities, repositories, services, utils. В entities создайте сущность Product:
package ru.java.rush.entities;

import lombok.Data;
import lombok.experimental.Accessors;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Accessors(chain = true)
@Entity
@Data
public class ProductEntity {

    @Id
    @Column
    @GenericGenerator(name = "generator", strategy = "increment")
    @GeneratedValue(generator = "generator")
    Integer id;

    @Column
    String name;

    @Column
    Integer purchasePrice;//закупочная цена

}
Реализуйте классы ProducRepository, ProducService и класс ItiiateUtil (аналогично прошлой статье). Допустим мы прикупли картофель по оптовой цене 20 рублей за кг., и морковки по 14 рублей за кг. Приобретенные продукты положим в хранилище. Дополним БД записями: [Id =1, name= “Картофель”, purchasePrice = 20] [Id =2, name= “Морковь”, purchasePrice = 14] Как порядочные спекулянты, мы должны выгодно впарить свой товар, для этого давайте красиво упакуем его и накрутим цену. То есть, были у нас грязные и не красивые овощи, наваленные кучей, а станут чистенькие премиум-веган продукты сегмента лакшери. Согласитесь, это будет уже не тот продукт(объект) который мы купили оптом. Для нового продукта создадим пакет dto и в нем класс ProductDto
package ru.java.rush.dto;

import lombok.Data;

@Data
public class ProductDto {
    Integer id;
    String name;
    Integer purchasePrice;
    String  packaging;//упаковка
    Integer salePrice;//цена реализации
}
У ProductDto есть две переменные, которых нет у ProductEntity: «упаковка» и «цена реализации». Объект dto может содержать точно такие же переменные, как и entity, или их может быть больше, или меньше. Мы помним, что конвертация одного объекта в другой – это дело маппинга. В пакете utils создадим класс MappingUtils
package ru.java.rush.utils;

import org.springframework.stereotype.Service;
import ru.java.rush.dto.ProductDto;
import ru.java.rush.entities.ProductEntity;

@Service
public class MappingUtils {
//из entity в dto
    public ProductDto mapToProductDto(ProductEntity entity){
        ProductDto dto = new ProductDto();
        dto.setId(entity.getId());
        dto.setName(entity.getName());
        dto.setPurchasePrice(entity.getPurchasePrice());
        return dto;
    }
//из dto в entity
    public ProductEntity mapToProductEntity(ProductDto dto){
        ProductEntity entity = new ProductEntity();
        entity.setId(dto.getId());
        entity.setName(dto.getName());
        entity.setPurchasePrice(dto.getPurchasePrice());
        return entity;
    }
}
Просто заполняем поля из одного объекта, аналогичными полями из другого объекта. В классе ProductService реализуем методы для поиска одного продукта или списка продуктов, но перед эти мы конвертируем entity в dto с помощь написанного выше метода.
private final ProductRepository productRepository;
private final MappingUtils mappingUtils;


//для листа продуктов мы использовали стрим
public List<ProductDto> findAll() {
    return productRepository.findAll().stream() //создали из листа стирим
            .map(mappingUtils::mapToProductDto) //оператором из streamAPI map, использовали для каждого элемента метод mapToProductDto из класса MappingUtils
.collect(Collectors.toList()); //превратили стрим обратно в коллекцию, а точнее в лист
}

//для одиночного продукта обошлись проще
public ProductDto findById(Integer id) {
    return mappingUtils.mapToProductDto( //в метод mapToProductDto
            productRepository.findById(id) //поместили результат поиска по id
                    .orElse(new ProductEntity()) //если ни чего не нашли, то вернем пустой entity
    );
}
Что будет если мы сейчас положим эти овощи на витрину? А давайте посмотрим. Для этого в ItiiateUtil напишем следующий код и запустим.
System.out.println("\nВитрина магазина");
for (ProductDto dto : productService.findAll()) {
    System.out.println(dto);
}
На выходе получим: Витрина магазина ProductDto(id=1, name=Картофель, purchasePrice=20, packaging=null, salePrice=null) ProductDto(id=2, name=Морковь, purchasePrice=14, packaging=null, salePrice=null) Ну уж, нет! Такие овощи никто не купит: грязные, не упакованы, да и цена продажи не известна. Настало время бизнес логики. Ее реализуем в классе ProductService. Добавим ка сначала в этот класс пару переменных:
private final Integer margin = 5;//это наша накрутка на цену
private final String packaging = "Упаковано в лучшем виде";//так будет выглядеть упаковка
Для каждого действия: упаковка и накрутка цены – создадим в этом же классе по отдельному методу:
// упаковываем товар
public void pack(List<ProductDto> list) {
    list.forEach(productDto ->
            productDto.setPackaging(packaging)
    );
}

// делаем деньги
public void makeMoney(List<ProductDto> list) {
    list.forEach(productDto ->
            productDto.setSalePrice(productDto.getPurchasePrice() * margin)
    );
}
Возвращаемся в ItiiateUtil и выкладывание на витрину заменяем на следующий код
List<ProductDto> productDtos = productService.findAll();

productService.pack(productDtos);
productService.makeMoney(productDtos);

System.out.println("\nВитрина магазина");
for (ProductDto dto : productDtos)) {
    System.out.println(dto);
}
Выполняем: Витрина магазина ProductDto(id=1, name=Картофель, purchasePrice=20, packaging=Упаковано в лучшем виде, salePrice=100) ProductDto(id=2, name=Морковь, purchasePrice=14, packaging=Упаковано в лучшем виде, salePrice=70) Товар красиво упакован, есть цена, но вы где-нибудь видели, что бы на витрине писали цену за которую купили оптом и еще id какой-то. Дорабатываем напильником, написанный выше код:
List<ProductDto> productDtos = productService.findAll();

productService.pack(productDtos);
productService.makeMoney(productDtos);

System.out.println("\nВитрина магазина");
for (ProductDto dto : productDtos) {
    System.out.println(String.format(
            "Купите: %s , по цене:  %d", dto.getName(), dto.getSalePrice()
    ));
}
class InitiateUtils в итоге должен быть таким:
@Service
@RequiredArgsConstructor
public class InitiateUtils implements CommandLineRunner {

    private final ProductService productService;

    @Override
    public void run(String... args) throws Exception {

        List<ProductEntity> products = new ArrayList<>(
                Arrays.asList(
                        new ProductEntity()
                                .setName("Картофель")
                                .setPurchasePrice(20),
                        new ProductEntity()
                                .setName("Морковь")
                                .setPurchasePrice(14)
                ));

        productService.saveAll(products);

        List<ProductDto> productDtos = productService.findAll();

        productService.pack(productDtos);
        productService.makeMoney(productDtos);

        System.out.println("\nВитрина магазина");
        for (ProductDto dto : productDtos) {
            System.out.println(String.format(
                    "Купите: %s , по цене:  %d", dto.getName(), dto.getSalePrice()
            ));
        }
    }
}
Запускаем: Витрина магазина Купите: Картофель , по цене: 100 Купите: Морковь , по цене: 70 Другое дело! Теперь по думаем, что dto принесло хорошего, кроме кучи дополнительного кода: 1. Мы можем совершать бизнес-логику не меняя объекты в БД(допустим ну ненужно нам в этой таблице иметь поля про упаковку и цену продажи). Картофель отлично пролежит в хранилище и без упаковки с ценником, они там даже лишние. 2. В этой строчке List<ProductDto> productDtos = productService.findAll() мы создали кэш из объектов с которыми удобно работать в рамках бизнес-логики. Это, если бы мы положили часть товара в подсобку магазина. 3. Это нам позволило, совершить два бизнес действия: упаковка и наценка, но запрос в базу сделали только один раз (запросы в базу довольно тяжелы в плане производительности). Товар можно упаковывать, клеить ценник и выкладывать на витрину - постепенно набирая его из подсобки, а не бегать за ним каждый раз в хранилище. На вопрос: «Зачем так сложно?», люди тоже пытаются найти ответ, почитайте. Следующая статья
Комментарии (7)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Vadim Zakirov Java Developer
9 октября 2021, 18:52
я за все статьи так и не понял, почему автор так не любит уровни доступа, тут в начале статьи, в других статьях тоже бывает, это что-то личное, или есть тактика и мы за ней следуем?
Павел
Уровень 11
9 октября 2021, 23:42
Поясни что ты имеешь ввиду под уровнями доступа, а то я не смогу пояснить почему я их не люблю)
Vadim Zakirov Java Developer
10 октября 2021, 06:58
вот тут, это критично оставлять без уровней или нет:
package ru.java.rush.dto;

import lombok.Data;

@Data
public class ProductDto {
    Integer id;
    String name;
    Integer purchasePrice;
    String  packaging;//упаковка
    Integer salePrice;//цена реализации
}
или тут
package ru.java.rush.entities;

import lombok.Data;
import lombok.experimental.Accessors;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Accessors(chain = true)
@Entity
@Data
public class ProductEntity {

    @Id
    @Column
    @GenericGenerator(name = "generator", strategy = "increment")
    @GeneratedValue(generator = "generator")
    Integer id;

    @Column
    String name;

    @Column
    Integer purchasePrice;//закупочная цена

}
15 сентября 2021, 22:44
мучает любопытство: а для чего все эти "за одно", "по смотрим", "где ни будь"?... это такой стёб, или Вы прям действительно "не гра мотны"? :)
Павел
Уровень 11
16 сентября 2021, 04:40
Это не стёб, я действительно не грамотен. Се ля ви🙄 Если напишете как надо правильно, с радостью поправлю.
16 сентября 2021, 06:00
был уверен, что стëб, прошу простить за наезд тогда. я сам не образец грамотности :) но если правда, то "заодно", "посмотрим", "где-нибудь"
Павел
Уровень 11
16 сентября 2021, 10:45
Поправил, спасибо! Если будет что то (что-то) еще, по орфографии или по java то пишите, буду рад.