GraphQL — это мощный инструмент для построения API, но, как известно, с большой мощью приходит большая ответственность. Даже самые опытные разработчики допускают ошибки при проектировании GraphQL-схем, обработчиков данных и запросов. В этой лекции мы разберем самые популярные ошибки, узнаем, как их избежать, и рассмотрим лучшие практики для написания качественного и производительного GraphQL API.
Типичные ошибки при проектировании схемы
Неполная или избыточная схема
Проектирование схемы — это своего рода искусство, и здесь легко перестараться или, наоборот, создать схему, которая не покрывает нужный функционал.
Пример ошибки:
type Query {
userProfile: UserProfile
}
type UserProfile {
id: ID
firstName: String
lastName: String
age: Int
hobbies: [String]
contactInfo: String
}
Выглядит нормально, но что, если contactInfo` содержит чувствительные данные? Такая схема легко может привести к утечке.
Как избежать:
- Проектируйте минималистичные и изолированные схемы.
- Используйте подход
need-to-knowпри предоставлении данных и избегайте "перекармливания" клиентов информацией.
Пример исправленной схемы:
type Query {
userProfile: UserProfile
}
type UserProfile {
id: ID
firstName: String
lastName: String
age: Int
hobbies: [String]
}
type PrivateUserInfo {
contactInfo: String
}
Теперь конфиденциальная информация вынесена в отдельный тип, который можно предоставлять только авторизованным пользователям.
Глубокие вложенные структуры (N+1 проблема)
Ошибка:
Если ваша схема состоит из слишком глубоких вложений, клиент может запрашивать огромные объемы данных одним мощным запросом.
Пример:
query {
users {
id
posts {
id
comments {
id
text
}
}
}
}
Если есть тысячи пользователей, у каждого десятки постов, а у каждого поста сотни комментариев, этот запрос может обанкротить ваш сервер.
Как избежать:
- Лимитируйте глубину запросов через плагины или middleware.
- Используйте
Batch Loadingдля устранения дублирующих запросов.
Ошибки в обработке данных
Проблемы с производительностью (N+1 ошибка)
Проблема: Популярная ошибка при использовании GraphQL связана с тем, что при извлечении связанных данных запросы к базе данных начинают выстреливать как из пулемета.
Пример:
@QueryMapping
public List<User> users() {
return userService.getAllUsers();
}
@QueryMapping
public List<Post> posts(@Argument("userId") String userId) {
return postService.getPostsByUser(userId);
}
Каждый пользователь может инициировать отдельный запрос для получения связанных постов. Если пользователей 100, то будет выполнено 100 SQL-запросов к базе данных.
Исправление: Используем DataLoader:
@Bean
public DataLoaderRegistry dataLoaderRegistry() {
DataLoaderRegistry registry = new DataLoaderRegistry();
DataLoader<String, List<Post>> postLoader = DataLoader.newMappedDataLoader(userService::getPostsForUsers);
registry.register("posts", postLoader);
return registry;
}
Теперь для 100 пользователей будет выполнено всего 1 батч-запрос.
Необработанные исключения
Ошибка: ваш обработчик данных выбрасывает необработанные исключения, которые идут в серверный лог и потенциально раскрывают информацию о внутреннем устройстве системы.
Пример:
@QueryMapping
public User user(@Argument("id") String id) {
return userService.findById(id);
}
Если пользователь с указанным id не найден, скорее всего, будет выброшено NullPointerException.
Решение: Оборачивайте ошибки в пользовательские исключения.
@QueryMapping
public User user(@Argument("id") String id) {
return userService.findById(id).orElseThrow(() -> new CustomException("User not found"));
}
Ошибки защиты данных
Отсутствие ограничений по глубине и сложности запросов
GraphQL позволяет клиенту работать настолько гибко, что неадекваты могут отправить запросы с глубиной в 1000 уровней или сложностью, превышающей лимит в 100 тысяч операций.
Решение:
- Включите ограничения глубины и сложности запросов.
- Используйте библиотеки таких как
graphql-javaили плагины для настройки лимитов.
GraphQLSchema schema = GraphQLSchema.newSchema()
.query(QueryType)
.maxQueryDepth(10)
.maxQueryComplexity(1000)
.build();
Без проверки авторизации и аутентификации
Ошибка: Часто забывают о проверке авторизации пользователей при выполнении запросов.
Решение:
Используйте DataFetchEnvironment, чтобы валидировать текущего пользователя:
public CompletableFuture<User> user(DataFetchingEnvironment env) {
String token = env.getContext();
if (!authService.isAuthenticated(token)) {
throw new AuthenticationException("Invalid token");
}
return userService.getUserByToken(token);
}
Ошибки в тестировании
Непокрытие тестами сложных запросов
Многие ограничиваются тестами для простых запросов, но забывают тестировать запросы с фрагментами, вложенными полями и мутациями.
Решение:
Создавайте тесты для всех сценариев использования. Например, используйте MockMvc для интеграционных тестов:
mockMvc.perform(post("/graphql")
.content("{\"query\":\"{ user { id, name } }\"}")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.user.id").exists());
Ошибки в проектировании API
Дублирование логики в схемах и резолверах
Ошибка: Логика повторяется для одного и того же поведения в GraphQL API.
Решение:
Используйте сервисный слой для обработки бизнес-логики и подгоняйте её под контекст запроса в резолверах.
Практика: исправление ошибок
В завершение лекции рассмотрим упражнения:
- Исправьте сценарий с N+1 проблемой с использованием
DataLoader. - Реализуйте ограничение глубины запросов.
- Напишите интеграционные тесты для мутации с вложенными объектами.
На реальном проекте это даст вам более устойчивое API, а клиенты будут вам благодарны.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ