Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. GraphQL со Strawberry
graphql_apollo

GraphQL со Strawberry

Schema, authorization, queries, mutations и request-scoped DataLoader

GraphQL в Django: Strawberry, контракты и безопасность

Статус: актуально для Django 5.2/6.0. Примеры используют Strawberry и strawberry-graphql-django. Graphene остаётся рабочей библиотекой для существующих проектов, но его API не следует смешивать с API Strawberry.

GraphQL — язык запросов и среда выполнения для типизированного API. Клиент выбирает поля из опубликованной схемы, но не получает произвольный доступ к ORM. Сервер по-прежнему отвечает за разрешения, границы выборки, стоимость запроса и согласованность данных.

GraphQL уменьшает число специальных endpoint для разных экранов и позволяет эволюционировать один типизированный контракт. Взамен команда получает более сложное кэширование, риск N+1, частичные ответы и необходимость ограничивать дорогие операции. Это архитектурный выбор, а не автоматическая замена REST.

#Когда GraphQL оправдан

Хорошие сигналы:

  • несколько клиентов запрашивают разные срезы связанных данных;
  • продукту полезны schema discovery и генерация типов;
  • frontend-команда часто меняет композицию экранов;
  • backend готов поддерживать единый граф и контролировать его стоимость.

REST или обычный Django view часто проще для небольшого CRUD, загрузки файлов, webhook, экспорта и endpoint с хорошо известной формой ответа. Один GraphQL HTTP request также не означает один SQL query: резолверы могут выполнить сотни обращений к БД.

#Схема — публичный контракт

Query читает данные, mutation изменяет их, subscription доставляет обновления. Типы GraphQL описывают внешний контракт и не обязаны один в один повторять Django models.

# models.py from django.conf import settings from django.db import models class Post(models.Model): author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) title = models.CharField(max_length=200) body = models.TextField() is_published = models.BooleanField(default=False)
# schema.py import strawberry import strawberry_django from .models import Post @strawberry_django.type(Post) class PostType: id: strawberry.auto title: strawberry.auto is_published: strawberry.auto @strawberry.type class Query: @strawberry_django.field def posts(self, info: strawberry.Info) -> list[PostType]: user = info.context.request.user queryset = Post.objects.order_by("-id") if not user.is_staff: queryset = queryset.filter(is_published=True) return queryset[:50]

Здесь ограничение результата и видимость заданы сервером. Опубликовать Post.objects.all() без pagination и object-level authorization — не нейтральное упрощение, а будущая утечка или отказ в обслуживании.

@strawberry.type class Mutation: @strawberry.mutation def rename_own_post( self, info: strawberry.Info, post_id: strawberry.ID, title: str, ) -> PostType: user = info.context.request.user if not user.is_authenticated: raise PermissionError("Authentication required") post = Post.objects.get(pk=post_id, author=user) post.title = title.strip() post.full_clean() post.save(update_fields=["title"]) return post schema = strawberry.Schema(query=Query, mutation=Mutation)

В production ожидаемые ошибки лучше возвращать типизированным payload/union с машинным code, а внутренние исключения логировать и скрывать. 200 OK с полем errors возможен, но GraphQL over HTTP использует и другие HTTP statuses для transport/request errors. Поэтому правило «GraphQL всегда возвращает 200» неверно.

#Django view и CSRF

# urls.py from django.urls import path from strawberry.django.views import GraphQLView from .schema import schema urlpatterns = [ path( "graphql/", GraphQLView.as_view( schema=schema, graphql_ide=None, allow_queries_via_get=False, ), ), ]

Современный Strawberry Django view не нужно автоматически оборачивать в csrf_exempt. При cookie/session authentication CSRF-защита должна сохраняться для mutations. Для bearer token модель угроз другая, но CORS, происхождение credentials и срок действия token всё равно требуют явной настройки.

GraphQL IDE можно отключить в production, однако это не является контролем доступа. Схему всё равно защищают authentication, authorization, query limits, rate limits и мониторинг. Отключение introspection также не делает известные операции безопасными.

#Authentication и authorization

Authentication обычно выполняет Django или внешний identity layer. Strawberry permission classes удобны для повторяемой проверки, но они не заменяют фильтрацию объектов.

from typing import Any import strawberry from strawberry.permission import BasePermission class IsAuthenticated(BasePermission): message = "Authentication required" def has_permission( self, source: Any, info: strawberry.Info, **kwargs: Any, ) -> bool: return info.context.request.user.is_authenticated @strawberry.type class AccountQuery: @strawberry.field(permission_classes=[IsAuthenticated]) def viewer_email(self, info: strawberry.Info) -> str: return info.context.request.user.email

Проверяйте разрешения на трёх уровнях:

  1. может ли субъект вызвать поле или mutation;
  2. какие строки он может увидеть/изменить;
  3. какие чувствительные поля допустимо раскрыть.

Особенно опасно резолвить объект сначала по id, а tenant/owner проверять после загрузки. Правильнее включить границу в queryset: Post.objects.get(pk=id, author=user) или использовать общий visible_to(user).

#N+1 и план загрузки

Запрос:

query Feed { posts { title author { username } } }

может породить один SQL query для posts и по одному для каждого author. Исправление зависит от резолвера:

  • select_related() для одиночной FK/OneToOne;
  • prefetch_related() для коллекций и M2M;
  • optimizer strawberry-graphql-django для поддерживаемых полей;
  • DataLoader для batching по ключам или обращений к внешнему сервису.

DataLoader не должен быть module-level singleton. Он кэширует значения, поэтому общий экземпляр способен отдать данные между requests и удерживать устаревшие объекты. Создавайте loader в GraphQL context каждого запроса.

from strawberry.dataloader import DataLoader from strawberry.django.views import GraphQLView from accounts.models import User async def load_users(keys: list[int]) -> list[User | ValueError]: rows = {row.pk: row async for row in User.objects.filter(pk__in=keys)} return [rows.get(key) or ValueError("User not found") for key in keys] class AppGraphQLView(GraphQLView): def get_context(self, request, response): return { "request": request, "response": response, "user_loader": DataLoader(load_fn=load_users), }

Batch function обязана вернуть столько элементов, сколько получила keys, и в том же порядке. После mutation очистите или обновите соответствующий request-local loader cache. DataLoader не заменяет pagination и не оптимизирует произвольные сложные фильтры.

#Контроль стоимости

Ограничение только глубины недостаточно. Плоский запрос с множеством aliases или огромным first тоже дорогой. Практический набор защиты:

  • pagination и верхние пределы first/limit на каждой коллекции;
  • depth/token/complexity limits с весами дорогих полей;
  • лимит размера HTTP body и времени выполнения;
  • rate limit по субъекту, operation и стоимости;
  • persisted/trusted operations для закрытых клиентов;
  • запрет batch amplification, если transport принимает несколько operations;
  • timeout/cancellation для downstream calls;
  • метрики duration, errors, selected operation name и resolver hotspots.

Не используйте полный текст query как metric label: это взрывает cardinality и может сохранить секретные arguments. Клиент должен присылать стабильное имя operation.

#Pagination и стабильность результата

Неограниченный posts: [Post!]! со временем станет проблемой. Для изменяемых больших наборов cursor/keyset pagination обычно стабильнее offset. Cursor должен кодировать детерминированный порядок, например (created_at, id), а сервер — ограничивать максимальный page size.

GraphQL позволяет вернуть data частично вместе с errors. Решите в контракте, какие поля nullable. Случайное добавление null в глубине non-null chain может обнулить родительский объект, поэтому nullability — часть бизнес-дизайна, а не оформление типов.

#Эволюция схемы

Добавление nullable поля обычно обратно совместимо. Переименование, удаление или ужесточение nullability может сломать клиентов. Безопасный путь:

  1. добавить новое поле;
  2. пометить старое deprecation_reason;
  3. измерить использование старого поля по operation registry;
  4. дать клиентам окно миграции;
  5. удалить поле в объявленный breaking release.

Schema snapshot и contract checks в CI выявляют случайные breaking changes. Для закрытых клиентов persisted operations показывают фактическую зависимость точнее, чем догадки.

#Subscriptions

Subscription — долгоживущий поток, а не бесплатный «real-time». Для Django потребуется ASGI-совместимая интеграция, authentication при соединении и повторная проверка authorization по мере жизни subscription. Также нужны heartbeat, backpressure, лимит соединений и корректное удаление consumer при disconnect.

Не публикуйте database event клиенту до commit. Для надёжных доменных уведомлений используйте transaction.on_commit() либо outbox, а channel layer рассматривайте как транспорт доставки, не как source of truth.

#Apollo Client

Apollo Client — один из frontend-клиентов GraphQL: он нормализует cache, выполняет queries/mutations и поддерживает subscriptions. Его наличие не требует Apollo Server на Django backend. Cache key и mutation update policy должны учитывать tenant и identity; иначе frontend может показать данные предыдущего пользователя.

#Legacy и исправления

  • Глобальный DataLoader заменён request-scoped экземпляром.
  • «GraphQL всегда возвращает 200» заменено правилами transport и partial response.
  • csrf_exempt больше не предлагается по умолчанию для session-auth endpoint.
  • Отключение GraphiQL/introspection не выдаётся за основную меру безопасности.
  • DataLoader больше не объявлен обязательным для каждой связи: ORM loading plan часто проще.
  • Graphene отмечен как legacy-вариант существующих проектов, а не как удалённая библиотека.
  • Apollo Client отделён от Django server implementation.

#Проверка перед production

  • схема не раскрывает внутренние models автоматически;
  • list fields имеют pagination и server-side maximum;
  • permissions покрывают operation, object и field;
  • query plan проверен на realistic selection sets;
  • loaders создаются на request и сохраняют порядок keys;
  • есть limits глубины/стоимости/размера/времени;
  • ошибки не раскрывают stack trace и secrets;
  • schema changes проходят compatibility check;
  • subscriptions имеют reconnect/backpressure/cleanup;
  • telemetry использует operation name, а не raw query.

#Материалы

  • Strawberry: Django integration
  • Strawberry: DataLoaders
  • Strawberry: permissions
  • GraphQL specification

Далее: Оптимизация производительности