Зарегистрируйтесь, чтобы продолжить обучение

Преобразование DTO в сущность для обновления Spring Boot

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

Возьмем для примера Post. Как правило, слаг в постах не меняется после создания. Если его изменить, все ссылки на пост перестанут работать и начнут выдавать ошибку 404. Технически это означает, что для обновления мы должны запретить изменять слаг. Фактически нам придется создать свой DTO для операции обновления.

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

Посмотрим, как выглядит наш DTO для создания сущности:

package io.hexlet.spring.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class PostCreateDTO {
    private String slug;
    private String name;
    private String body;
}

В таком случае DTO для обновления будет выглядеть так:

package io.hexlet.spring.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class PostUpdateDTO {
    private String name;
    private String body;
}

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

var post = postRepository.findById(id);
post.setName(dto.getName());
// остальной код
postRepository.save(post); // UPDATE

Учитывая все сказанное выше, мы получим следующий обработчик в контроллере:

@RestController
@RequestMapping("/api")
public class PostsController {
    @Autowired
    private PostRepository repository;

    @PutMapping("/posts/{id}")
    @ResponseStatus(HttpStatus.OK)
    public PostDTO update(@RequestBody @Valid PostUpdateDTO postData, @PathVariable Long id) {
        var post = repository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Not Found"));
        toEntity(postData, post);
        repository.save(post);
        var postDTO = toDTO(post);
        return postDTO;
    }

    private Post toEntity(PostUpdateDTO postDTO, Post post) {
        post.setName(postDTO.getName());
        post.setBody(postDTO.getBody());
        return post;
    }

    private PostDTO toDTO(Post post) {
        var dto = new PostDTO();
        dto.setId(post.getId());
        dto.setSlug(post.getSlug());
        dto.setName(post.getName());
        dto.setBody(post.getBody());
        dto.setCreatedAt(post.getCreatedAt());
        return dto;
    }
}

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


Самостоятельная работа

При проектировании API важно отделять внутреннюю модель данных от внешнего интерфейса. Это дает больше гибкости и безопасности: мы сами определяем, какие поля клиент может изменять, а какие должны оставаться под контролем системы. Один из лучших способов добиться этого — использовать DTO (Data Transfer Object).

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

  1. Создайте DTO для обновления

    • В DTO добавьте только те поля, которые можно изменять.
    • Добавьте аннотации валидации (@NotBlank, @Size и т.д.).

      Пример: PostUpdateDTO
      import jakarta.validation.constraints.NotBlank;
      import jakarta.validation.constraints.Size;
      
      public class PostUpdateDTO {
      
          @NotBlank
          @Size(min = 3, max = 100)
          private String title;
      
          @NotBlank
          @Size(min = 10)
          private String content;
      
          // геттеры/сеттеры
      }
      
  2. Измените контроллер для использования DTO

    Пример: метод обновления поста
    @PutMapping("/{id}")
    public ResponseEntity<PostDTO> updatePost(
            @PathVariable Long id,
            @Valid @RequestBody PostUpdateDTO dto) {
    
        var post = postRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    
        post.setTitle(dto.getTitle());
        post.setContent(dto.getContent());
        post.setUpdatedAt(LocalDateTime.now());
    
        postRepository.save(post);
    
        var response = new PostDTO();
        response.setId(post.getId());
        response.setTitle(post.getTitle());
        response.setContent(post.getContent());
        response.setPublished(post.isPublished());
        response.setCreatedAt(post.getCreatedAt());
        response.setUpdatedAt(post.getUpdatedAt());
    
        return ResponseEntity.ok(response);
    }
    
  3. Добавьте обработку ошибок валидации

    • Перехватывайте MethodArgumentNotValidException.
    • Возвращайте 422 Unprocessable Entity и список ошибок в JSON.

      Пример: GlobalExceptionHandler
      @RestControllerAdvice
      public class GlobalExceptionHandler {
      
          @ExceptionHandler(MethodArgumentNotValidException.class)
          public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
              Map<String, String> errors = new HashMap<>();
              ex.getBindingResult().getFieldErrors().forEach(error ->
                      errors.put(error.getField(), error.getDefaultMessage())
              );
              return ResponseEntity.unprocessableEntity().body(errors);
          }
      }
      
  4. Проверьте работу

    • Отправьте PUT-запрос с пустыми или некорректными полями.
    • Убедитесь, что сервер возвращает статус 422 и список ошибок.

Итог

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

  • Четко контролировать, какие поля можно изменять.
  • Изолировать внутренние сущности от внешнего API.
  • Удобно валидировать данные и возвращать понятные ошибки клиенту.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff