Помимо преобразования в DTO, существует и обратная задача — преобразование DTO в Entity. Зачем это делать, если можно наполнять сущность напрямую?
Первая причина — это безопасность. Когда у нас есть API для создания или изменения сущности, обычно мы хотим дать возможность менять только часть свойств. Но если мы используем в @RequestBody нашу сущность напрямую, то у клиента API появляется возможность поменять любые свойства сущности:
@PutMapping("/users/{id}")
@ResponseStatus(HttpStatus.OK)
// Клиенты могут менять все свойства внутри пользователя
public UserDTO update(@RequestBody User user, @PathVariable Long id) {
    repository.save(user);
}
Мы не советуем использовать userData как сущность и сразу сохранять в базу — такой подход создает потенциальную опасность.
Вторая причина — схема данных. Со временем именование свойств может меняться и в базе данных, и в API — например, при внедрении новой версии. Разделение сущностей и DTO позволяет делать это независимо. DTO представляет внешний интерфейс для API. В свою очередь, сущности описывают внутреннюю модель данных.
Кроме того, существует еще несколько причин, которые мы разберем подробнее в других уроках:
- Дополнительные преобразования данных перед тем, как они попадут в сущность — например, нормализация электронной почты.
- Дополнительная валидация, которая может понадобиться в конкретном API. Хорошим примером служит подтверждение пароля. Подтверждение пароля не существует на уровне сущности, это вопрос проверки корректности входных данных.
Преобразование из сущности в DTO и наоборот обычно отличаются набором свойств. Например, в большинстве случаев идентификатор генерируется в базе данных — мы не хотим передавать его в API. При этом при возврате ответа в API мы хотим вернуть идентификатор среди остальных свойств. Поэтому есть смысл создавать разные DTO для этих задач.
Разберем пример с созданием и выводом сущности Post:
package io.hexlet.spring.model;
import static jakarta.persistence.GenerationType.IDENTITY;
import java.time.LocalDate;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
@Table(name = "posts")
@EntityListeners(AuditingEntityListener.class)
public class Post {
    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;
    @Column(unique = true)
    private String slug;
    private String name;
    @Column(columnDefinition = "TEXT")
    private String body;
    @CreatedDate
    private LocalDate createdAt;
}
Создадим два DTO для каждого действия. Создание потребует три поля — slug, name и body. В вывод добавятся поля id и createdAt:
// Создание поста
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;
}
// Вывод поста
package io.hexlet.spring.dto;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class PostDTO {
    private String id;
    private String slug;
    private String name;
    private String body;
    private LocalDate createdAt;
}
Реализуем создание и вывод. Вывод потребует преобразования только в DTO, а создание — оба преобразования (из сущности в DTO и наоборот):
package io.hexlet.spring.controller.api;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import io.hexlet.spring.dto.PostCreateDTO;
import io.hexlet.spring.dto.PostDTO;
import io.hexlet.spring.model.Post;
import io.hexlet.spring.exception.ResourceNotFoundException;
import io.hexlet.spring.repository.PostRepository;
@RestController
@RequestMapping("/api")
public class PostsController {
    @Autowired
    private PostRepository repository;
    @GetMapping("/posts/{id}")
    @ResponseStatus(HttpStatus.OK)
    public PostDTO show(@PathVariable Long id) {
        var post = repository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Not Found: " + id));
        var postDTO = toDTO(post); // Только в DTO
        return postDTO;
    }
    @PostMapping("/posts")
    @ResponseStatus(HttpStatus.CREATED)
    public PostDTO create(@RequestBody PostCreateDTO postData) {
        var post = toEntity(postData); // Сначала в Entity
        repository.save(post);
        var postDTO = toDTO(post); // Потом в DTO
        return postDTO;
    }
    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;
    }
    private Post toEntity(PostCreateDTO postDTO) {
        var post = new Post();
        post.setSlug(postDTO.getSlug());
        post.setName(postDTO.getName());
        post.setBody(postDTO.getBody());
        return post;
    }
}
В методе create() мы поменяли тип входных данных на PostCreateDTO. Уже внутри эти данные копируются в только что созданный объект post. После сохранения в базу данных мы снова выполняем преобразование из Post в PostDTO, чтобы сформировать тело ответа. На этом моменте проявляется разница между тем, что приходит на вход, и тем, что должно быть на выходе. Например, идентификатор появляется только после того, как мы выполняем сохранение в базу данных. Поэтому мы получаем такую цепочку: PostCreateDTO => Post => PostDTO.
Самостоятельная работа
Теперь мы разделим входные данные от моделей базы и обеспечим валидацию.
- Создаём DTO для входящих запросов - DTO должны содержать только необходимые для запроса поля.
- Добавьте валидацию с помощью - jakarta.validationаннотаций.- Пример: PostCreateDTO- import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; public class PostCreateDTO { @NotBlank @Size(min = 3, max = 100) private String title; @NotBlank @Size(min = 10) private String content; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } }
 
- Изменяем контроллер для использования DTO - Пример: PostController (создание поста)- @RestController @RequestMapping("/api/posts") public class PostController { private final PostRepository postRepository; public PostController(PostRepository postRepository) { this.postRepository = postRepository; } @PostMapping public ResponseEntity<PostDTO> createPost(@Valid @RequestBody PostCreateDTO dto) { var post = new Post(); post.setTitle(dto.getTitle()); post.setContent(dto.getContent()); post.setPublished(true); post.setCreatedAt(LocalDateTime.now()); 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.status(HttpStatus.CREATED).body(response); } }
- Обрабатываем ошибки валидации - Spring Boot сам проверяет @Validпараметры и выбрасываетMethodArgumentNotValidException.
- Можно добавить глобальный обработчик, чтобы вернуть красивый JSON-ответ со статусом 422 Unprocessable Entity. - Пример: 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); } }
 
- Spring Boot сам проверяет 
Проверяем работу
- Отправьте POST-запрос без обязательных полей или с коротким текстом.
- Убедитесь, что сервер вернёт JSON с описанием ошибок и статусом 422.
Итог: контроллеры больше не принимают сущности напрямую. Мы используем входные DTO с валидацией, а ошибки красиво обрабатываются.
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.