Schema, authorization, queries, mutations и request-scoped DataLoader
Статус: актуально для Django 5.2/6.0. Примеры используют Strawberry и
strawberry-graphql-django. Graphene остаётся рабочей библиотекой для существующих проектов, но его API не следует смешивать с API Strawberry.
GraphQL — язык запросов и среда выполнения для типизированного API. Клиент выбирает поля из опубликованной схемы, но не получает произвольный доступ к ORM. Сервер по-прежнему отвечает за разрешения, границы выборки, стоимость запроса и согласованность данных.
GraphQL уменьшает число специальных endpoint для разных экранов и позволяет эволюционировать один типизированный контракт. Взамен команда получает более сложное кэширование, риск N+1, частичные ответы и необходимость ограничивать дорогие операции. Это архитектурный выбор, а не автоматическая замена REST.
Хорошие сигналы:
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» неверно.
# 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 обычно выполняет 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Проверяйте разрешения на трёх уровнях:
Особенно опасно резолвить объект сначала по id, а tenant/owner проверять после загрузки. Правильнее включить границу в queryset: Post.objects.get(pk=id, author=user) или использовать общий visible_to(user).
Запрос:
query Feed {
posts {
title
author { username }
}
}может породить один SQL query для posts и по одному для каждого author. Исправление зависит от резолвера:
select_related() для одиночной FK/OneToOne;prefetch_related() для коллекций и M2M;strawberry-graphql-django для поддерживаемых полей;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 тоже дорогой. Практический набор защиты:
first/limit на каждой коллекции;Не используйте полный текст query как metric label: это взрывает cardinality и может сохранить секретные arguments. Клиент должен присылать стабильное имя operation.
Неограниченный posts: [Post!]! со временем станет проблемой. Для изменяемых больших наборов cursor/keyset pagination обычно стабильнее offset. Cursor должен кодировать детерминированный порядок, например (created_at, id), а сервер — ограничивать максимальный page size.
GraphQL позволяет вернуть data частично вместе с errors. Решите в контракте, какие поля nullable. Случайное добавление null в глубине non-null chain может обнулить родительский объект, поэтому nullability — часть бизнес-дизайна, а не оформление типов.
Добавление nullable поля обычно обратно совместимо. Переименование, удаление или ужесточение nullability может сломать клиентов. Безопасный путь:
deprecation_reason;Schema snapshot и contract checks в CI выявляют случайные breaking changes. Для закрытых клиентов persisted operations показывают фактическую зависимость точнее, чем догадки.
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 — один из frontend-клиентов GraphQL: он нормализует cache, выполняет queries/mutations и поддерживает subscriptions. Его наличие не требует Apollo Server на Django backend. Cache key и mutation update policy должны учитывать tenant и identity; иначе frontend может показать данные предыдущего пользователя.
DataLoader заменён request-scoped экземпляром.csrf_exempt больше не предлагается по умолчанию для session-auth endpoint.Далее: Оптимизация производительности