Создание и обновление сущности — это похожие операции, но все таки они совпадают не полностью. Различия между операциями приводят к тому, что нам нужно создавать свои 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 для обновления сущностей и подключите базовую валидацию данных.
Создайте 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; // геттеры/сеттеры }
Измените контроллер для использования 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); }
Добавьте обработку ошибок валидации
- Перехватывайте
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); } }
- Перехватывайте
Проверьте работу
- Отправьте PUT-запрос с пустыми или некорректными полями.
- Убедитесь, что сервер возвращает статус 422 и список ошибок.
Итог
Теперь методы обновления работают через DTO, что позволяет:
- Четко контролировать, какие поля можно изменять.
- Изолировать внутренние сущности от внешнего API.
- Удобно валидировать данные и возвращать понятные ошибки клиенту.
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.