Pull to refresh

Еще один способ использования Java records как DTO

Reading time3 min
Views8.5K

В данной статье будет рассмотрен способ применения Java records в качестве DTO (data transfer objects).


Используем Spring Boot / Hibernate.


Представленный далее код не предназначен для продакшена. Это, скорее, размышления на тему. Возможно кому-то будет интересно и полезно.


Цель — за пределами сервисного слоя использовать только DTO и не таскать сущности с persistence context'ом по бизнес-логике.


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


По сути имеем три задачи — сохранение/выборку данных и удобный способ работы с ними.


В демо проекте реализованы две сущности — Note и Tag, со связями ManyToMany.


Далее описаны способы выборки и обновления данных на примере Tag.


Для выборки данных в repository используем JPQL Constructor Expressions:


    @Transactional(readOnly = true)
    @Query(value = "SELECT new dev.isdn.demo.records_dto.app.domain.tag.TagDto(t.id, t.name, t.color) FROM tags t WHERE t.id = :id")
    Optional<TagDto> findById(@Param("id") long id);

Получаем результат сразу в record, завернутый в Optional. Если выборка пустая — то получаем пустой Optional.


Соответственно, в сервисном слое просто передаем результат:


    public Optional<TagDto> getTagById(long tagId) {
        return repository.findById(tagId);
    }

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


Optional.ofNullable(tagDto)
.flatMap(t -> repository.getTagById(t.id())) // проверяем наличие записи с таким ID в базе и преобразуем DTO в entity
.flatMap(t -> setTagNameAndColor(t, content.name(), content.color())) // обновляем entity

В данном случае очень удобно использовать фичи Optional для проверки результата на каждом шаге.


Метод setTagNameAndColor() выполняет проверку входных данных и обновляет сущность.


В итоге сделал вот такой сервисный метод в декларативном стиле:


    @Transactional
    public Optional<TagDto> updateTagContent(TagDto tag, TagContent content) {
        return Functions.checkTagDto.apply(tag)
                .flatMap(t -> repository.getTagById(t.id()))
                .flatMap(t -> setTagNameAndColor(t, content.name(), content.color()))
                .map(repository::saveAndFlush)
                .flatMap(t -> repository.findById(t.getId()));
    }

Такой подход выглядит немного избыточным, сделано исключительно в демонстрационных целях.


Функция checkTagDto делает простую проверку Optional.ofNullable(tag).filter(t -> t.id() > 0).


Более сложные выборки из базы в repository можно делать аналогично.
Например, получение связанных данных:


    @QueryHints(value = {
            @QueryHint(name = HINT_FETCH_SIZE, value = "100"),
            @QueryHint(name = READ_ONLY, value = "true")
    })
    @Transactional(readOnly = true)
    @Query(value = "SELECT new dev.isdn.demo.records_dto.app.domain.tag.TagDto(t.id, t.name, t.color) FROM tags t INNER JOIN t.notes n WHERE n.id = :noteId")
    Stream<TagDto> findAllByNoteId(@Param("noteId") long noteId);

    @QueryHints(value = {
            @QueryHint(name = HINT_FETCH_SIZE, value = "100"),
            @QueryHint(name = READ_ONLY, value = "true")
    })
    @Transactional(readOnly = true)
    @Query(value = "SELECT new dev.isdn.demo.records_dto.app.domain.note.NoteDto(n.id, n.created, n.modified, n.content) FROM notes n INNER JOIN n.tags t WHERE t.id = :tagId")
    Stream<NoteDto> findAllByTagId(@Param("tagId") long tagId);

Теперь о том, как с этим работать.


Простой пример REST контроллера:


    @PutMapping(PREFIX + VERSION + "/tags/{id}")
    TagDto updateTagContent(@PathVariable long id, @RequestBody TagContent content) {
        TagDto tag = tagService.getTagById(id).orElseThrow(() -> new NoSuchItemException("tag " + id));
        return tagService.updateTagContent(tag, content).orElseThrow(() -> new NotUpdatedException("tag " + id));
    }

    @GetMapping(PREFIX + VERSION + "/tags/{id}/notes")
    List<NoteDto> getTagNotes(@PathVariable long id) {
        TagDto tag = tagService.getTagById(id).orElseThrow(() -> new NoSuchItemException("tag " + id));
        return noteService.getTagNotes(tag);
    }

Полностью код можно посмотреть вот здесь.

Tags:
Hubs:
Total votes 4: ↑3 and ↓1+2
Comments24

Articles