13 мая 2024
Тэги: Collections, GraphQL, json, Kotlin, Spring Boot, руководство.
Продолжаем цикл статей про работу с GraphQL в проекте, написанном на Kotlin и Spring Boot. В первой статье GraphQL в Spring Boot мы создали проект, который возвращает информацию о книгах и их авторах. Во второй части Мутации в GraphQL мы научились менять данные.
Теперь рассмотрим как можно обрабатывать ошибки в GraphQL и менять их формат.
Добавим в наш проект новый метод, возвращающий информацию о книге по её id.
Модифицируем файл schema.graphqls, добавив в тип Query второй метод под названием getBookById():
Этот метод всегда должен возвращать книгу.
Теперь добавим новый метод findById() в BookRepository. В реальном проекте этот репозиторий читал бы данные из БД, но в нашем примере данные захардкожены. Для удобства список всех книг превратим в мапу, чтобы быстрее делать выборку по id.
Обратите внимание, что если книга не найдена, то данный метод вернёт null и никакой ошибки не возникнет. Это сделано специально, т.к. на уровне репозитория недостаточно контекста для принятия решения о том, допустимо ли возвращать null или нет. Обработку ошибок мы сделаем на уровне выше. Благодаря такому подходу один и тот же метод репозитория может быть переиспользован в разных «бизнесовых» методах.
Теперь осталось добавить метод getBookById() в класс BookQuery. Сигнатура метода должна соответствовать той, которую мы объявили в graphql-схеме. Каждый аргумент graphql-метода должен быть помечен аннотацией @Argument.
Здесь мы вызываем метод репозитория. Если книга с указанным id найдена – мы вернём эту книгу. Если не найдена – репозиторий вернёт null и благодаря элвис-оператору мы кидаем RuntimeException с соответствующим описанием ошибки.
Теперь запустим проект и протестируем новый метод через веб-интерфейс, доступный по адресу http://127.0.0.1:8080/graphiql. Запрос может выглядеть как-то так:
В ответ получим одну книгу из тех, что мы захардкодили:
Обратите внимание, что тут мы вместе с книгой в новом методе запрашиваем ещё и информацию об авторе, хотя никакой логики по работе с авторами мы сейчас не написали. Graphql автоматически переиспользует логику, которую мы делали ранее для списка всех книг. Он уже знает, как связаны между собой книги и авторы! И в этом проявляется вся мощь механизма graphql.
Теперь, если мы запросим книгу с каким-то произвольным id, то в ответ получим ошибку:
Здесь мы наблюдаем стандартный формат ответа graphql. Он содержит по сути лишь название метода, в котором произошла ошибка. Возможно, с точки зрения безопасности вы и не захотите отдавать больше информации клиенту. Но как быть, если всё-таки нам нужно вернуть более детальную ошибку? Ответ прост: надо создать собственный формат ответа при ошибке.
Хоть это и не обязательно, но очень удобно иметь базовый абстрактный класс исключения для всех ошибок в бизнес-логике.
Этот класс мы наследуем от RuntimeException, в который передаем текст сообщения об ошибке. Помимо текста, наше исключение также содержит расширенную информацию: код ошибки, тип ошибки graphql и дополнительные опциональные параметры.
Код ошибки обычно делают числовым, но я предпочитаю текстовый, т.к. он более читаемый. Это именно код, т.е. заранее определённое значение строки, которое является частью протокола взаимодействия между клиентом и сервером.
Тип ошибки graphql – это перечисление ErrorType из библиотеки graphql, в котором всего 5 возможных значений.
Теперь создадим исключение BookNotFoundException специально для ситуации, когда книга не найдена. Оно наследуется от нашего базового исключения.
Исключение принимает на вход лишь id книги, по которому производился поиск. Всё остальное определяем в самом исключении, в том числе текстовый код ошибки book.not.found и текстовое описание.
Такой подход позволяет сократить объём кода в месте возникновения ошибки:
Теперь осталось определить бин, который меняет формат ответа. Его мы унаследуем от DataFetcherExceptionResolverAdapter.
Логика работы метода проста: если мы получили исключение бизнес-логики (т.е. исключение, производное от BusinessLogicException), то заполняем параметры ошибки из него. Если это любое другое исключение – значит оно, скорее всего, общее или техническое, поэтому подставляем там какие-то параметры по умолчанию.
Метод расширения getBusinessError() выглядит так:
Описание ошибки, тип ошибки мы извлекаем из исключения и помещаем в соответствующие методы. А всю дополнительную информацию, в том числе код ошибки, мы передаём как мапу в секцию extensions.
Метод расширения getInternalError() похож на предыдущий, но недостающие параметры мы заполняем значениями по умолчанию:
Все подобные исключения получают общий код ошибки internal.error.
Теперь выполним запрос несуществующей книги ещё раз и убедимся, что ответ стал более информативным:
По сравнению с изначальным вариантом, тут мы видим уже текстовое описание, а также код ошибки и id, по которому выполняется поиск в секции extensions.
Мы создали новый метод для поиска книги по её id и ещё раз убедились, что graphql автоматически переиспользует ранее написанный код для подгрузки связанной сущности автора книги. Затем увидели, что Spring Boot Graphql предоставляет простой способ кастомизации ошибок, а наличие общего базового исключения для всех ошибок бизнес-логики позволяет избежать лишнего дублирования кода и при этом несёт дополнительную информацию.
Однако всегда надо помнить, что добавление деталей ошибки в ответ клиенту может нарушить безопасность приложения.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.