GraphQL API в Django

GraphQL позволяет клиентам запрашивать точно те данные, которые им нужны, избегая проблем over-fetching и under-fetching, характерных для REST API.

Установка и настройка Graphene

1# Установка необходимых пакетов
2pip install graphene-django
3pip install django-graphql-jwt  # для JWT аутентификации
 1# settings.py
 2INSTALLED_APPS = [
 3    'django.contrib.admin',
 4    'django.contrib.auth',
 5    'django.contrib.contenttypes',
 6    'django.contrib.sessions',
 7    'django.contrib.messages',
 8    'django.contrib.staticfiles',
 9    'graphene_django',
10    'your_app',
11]
12
13GRAPHENE = {
14    'SCHEMA': 'your_app.schema.schema',
15    'MIDDLEWARE': [
16        'graphql_jwt.middleware.JSONWebTokenMiddleware',
17    ],
18}
19
20AUTHENTICATION_BACKENDS = [
21    'graphql_jwt.backends.JSONWebTokenBackend',
22    'django.contrib.auth.backends.ModelBackend',
23]

Создание базовой схемы

 1# your_app/schema.py
 2import graphene
 3from graphene_django import DjangoObjectType
 4from django.contrib.auth.models import User
 5from .models import Book, Author, Category
 6
 7class UserType(DjangoObjectType):
 8    class Meta:
 9        model = User
10        fields = ('id', 'username', 'email', 'first_name', 'last_name')
11
12class CategoryType(DjangoObjectType):
13    class Meta:
14        model = Category
15        fields = '__all__'
16
17class AuthorType(DjangoObjectType):
18    class Meta:
19        model = Author
20        fields = '__all__'
21
22class BookType(DjangoObjectType):
23    class Meta:
24        model = Book
25        fields = '__all__'
26
27class Query(graphene.ObjectType):
28    # Получить все книги
29    all_books = graphene.List(BookType)
30
31    # Получить книгу по ID
32    book = graphene.Field(BookType, id=graphene.Int(required=True))
33
34    # Получить книги по категории
35    books_by_category = graphene.List(BookType, category_id=graphene.Int())
36
37    # Получить все категории
38    all_categories = graphene.List(CategoryType)
39
40    # Получить всех авторов
41    all_authors = graphene.List(AuthorType)
42
43    # Поиск книг по названию
44    search_books = graphene.List(BookType, query=graphene.String())
45
46    def resolve_all_books(self, info):
47        return Book.objects.all()
48
49    def resolve_book(self, info, id):
50        try:
51            return Book.objects.get(pk=id)
52        except Book.DoesNotExist:
53            return None
54
55    def resolve_books_by_category(self, info, category_id):
56        return Book.objects.filter(category_id=category_id)
57
58    def resolve_all_categories(self, info):
59        return Category.objects.all()
60
61    def resolve_all_authors(self, info):
62        return Author.objects.all()
63
64    def resolve_search_books(self, info, query):
65        return Book.objects.filter(title__icontains=query)
66
67schema = graphene.Schema(query=Query)

Создание мутаций

 1# your_app/mutations.py
 2import graphene
 3from graphene_django.forms.mutation import DjangoModelFormMutation
 4from .forms import BookForm, AuthorForm
 5from .models import Book, Author
 6
 7class CreateBookMutation(DjangoModelFormMutation):
 8    class Meta:
 9        form_class = BookForm
10
11class UpdateBookMutation(DjangoModelFormMutation):
12    class Meta:
13        form_class = BookForm
14
15class DeleteBookMutation(graphene.Mutation):
16    class Arguments:
17        id = graphene.Int(required=True)
18
19    success = graphene.Boolean()
20    message = graphene.String()
21
22    def mutate(self, info, id):
23        try:
24            book = Book.objects.get(pk=id)
25            book.delete()
26            return DeleteBookMutation(success=True, message="Книга успешно удалена")
27        except Book.DoesNotExist:
28            return DeleteBookMutation(success=False, message="Книга не найдена")
29
30class CreateAuthorMutation(DjangoModelFormMutation):
31    class Meta:
32        form_class = AuthorForm
33
34class Mutation(graphene.ObjectType):
35    create_book = CreateBookMutation.Field()
36    update_book = UpdateBookMutation.Field()
37    delete_book = DeleteBookMutation.Field()
38    create_author = CreateAuthorMutation.Field()
39
40# Обновляем схему
41schema = graphene.Schema(query=Query, mutation=Mutation)

Формы для мутаций

 1# your_app/forms.py
 2from django import forms
 3from .models import Book, Author
 4
 5class BookForm(forms.ModelForm):
 6    class Meta:
 7        model = Book
 8        fields = ['title', 'description', 'author', 'category', 'published_date', 'price']
 9
10class AuthorForm(forms.ModelForm):
11    class Meta:
12        model = Author
13        fields = ['name', 'bio', 'birth_date']

Аутентификация и авторизация

 1# your_app/schema.py
 2import graphene
 3from graphql_jwt.decorators import login_required, permission_required
 4from django.contrib.auth.models import Permission
 5
 6class UserProfileType(DjangoObjectType):
 7    class Meta:
 8        model = User
 9        fields = ('id', 'username', 'email', 'first_name', 'last_name', 'is_staff')
10
11class Query(graphene.ObjectType):
12    # ... существующие поля ...
13
14    # Получить профиль текущего пользователя
15    me = graphene.Field(UserProfileType)
16
17    @login_required
18    def resolve_me(self, info):
19        return info.context.user
20
21class CreateBookMutation(DjangoModelFormMutation):
22    class Meta:
23        form_class = BookForm
24
25    @login_required
26    def mutate_and_get_payload(self, info, input):
27        # Проверяем права на создание книг
28        if not info.context.user.has_perm('your_app.add_book'):
29            raise Exception("У вас нет прав на создание книг")
30        return super().mutate_and_get_payload(info, input)

Подписки (Subscriptions)

 1# your_app/subscriptions.py
 2import graphene
 3from graphql_jwt.decorators import login_required
 4from channels_graphql_ws import Subscription
 5
 6class BookSubscription(Subscription):
 7    class Arguments:
 8        category_id = graphene.Int()
 9
10    book = graphene.Field(BookType)
11    operation = graphene.String()
12
13    def subscribe(self, info, category_id=None):
14        if category_id:
15            return [f"book_updates_{category_id}"]
16        return ["book_updates"]
17
18    def publish(self, info, book, operation):
19        return BookSubscription(book=book, operation=operation)
20
21class Subscription(graphene.ObjectType):
22    book_updates = BookSubscription.Field()
23
24# Обновляем схему
25schema = graphene.Schema(
26    query=Query,
27    mutation=Mutation,
28    subscription=Subscription
29)

URLs для GraphQL

 1# your_app/urls.py
 2from django.urls import path
 3from graphene_django.views import GraphQLView
 4from django.views.decorators.csrf import csrf_exempt
 5from .schema import schema
 6
 7urlpatterns = [
 8    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
 9    path('graphql/api/', csrf_exempt(GraphQLView.as_view(graphiql=False, schema=schema))),
10]

Примеры GraphQL запросов

 1# Получить все книги с авторами и категориями
 2query {
 3    allBooks {
 4        id
 5        title
 6        description
 7        publishedDate
 8        price
 9        author {
10            id
11            name
12            bio
13        }
14        category {
15            id
16            name
17        }
18    }
19}
20
21# Создать новую книгу
22mutation {
23    createBook(input: {
24        title: "Новая книга"
25        description: "Описание новой книги"
26        authorId: 1
27        categoryId: 1
28        publishedDate: "2024-01-01"
29        price: "29.99"
30    }) {
31        book {
32            id
33            title
34            author {
35                name
36            }
37        }
38            errors {
39                field
40                messages
41            }
42    }
43}
44
45# Поиск книг
46query {
47    searchBooks(query: "Python") {
48        id
49        title
50        author {
51            name
52        }
53    }
54}

Обработка ошибок

 1# your_app/exceptions.py
 2import graphene
 3from graphql import GraphQLError
 4
 5class BookNotFoundError(GraphQLError):
 6    def __init__(self, book_id):
 7        super().__init__(f"Книга с ID {book_id} не найдена")
 8
 9class InsufficientPermissionsError(GraphQLError):
10    def __init__(self, action):
11        super().__init__(f"Недостаточно прав для выполнения действия: {action}")
12
13# В резолверах
14def resolve_book(self, info, id):
15    try:
16        return Book.objects.get(pk=id)
17    except Book.DoesNotExist:
18        raise BookNotFoundError(id)

Тестирование GraphQL API

 1# your_app/tests/test_graphql.py
 2import json
 3from django.test import TestCase
 4from graphene_django.utils.testing import GraphQLTestCase
 5from django.contrib.auth.models import User
 6from .models import Book, Author, Category
 7
 8class BookGraphQLTest(GraphQLTestCase):
 9    GRAPHQL_URL = '/graphql/'
10
11    def setUp(self):
12        self.user = User.objects.create_user(
13            username='testuser',
14            password='testpass123'
15        )
16        self.author = Author.objects.create(name='Тестовый автор')
17        self.category = Category.objects.create(name='Техническая литература')
18        self.book = Book.objects.create(
19            title='Тестовая книга',
20            author=self.author,
21            category=self.category
22        )
23
24    def test_query_all_books(self):
25        response = self.query(
26            '''
27            query {
28                allBooks {
29                    id
30                    title
31                    author {
32                        name
33                    }
34                }
35            }
36            ''',
37            op_name='allBooks'
38        )
39        self.assertResponseNoErrors(response)
40        content = json.loads(response.content)
41        self.assertEqual(len(content['data']['allBooks']), 1)
42        self.assertEqual(content['data']['allBooks'][0]['title'], 'Тестовая книга')
43
44    def test_create_book_mutation(self):
45        response = self.query(
46            '''
47            mutation {
48                createBook(input: {
49                    title: "Новая книга"
50                    authorId: 1
51                    categoryId: 1
52                }) {
53                    book {
54                        id
55                        title
56                    }
57                    errors {
58                        field
59                        messages
60                    }
61                }
62            }
63            ''',
64            op_name='createBook'
65        )
66        self.assertResponseNoErrors(response)

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

 1# your_app/schema.py
 2from django.db.models import Prefetch
 3
 4class Query(graphene.ObjectType):
 5    # ... существующие поля ...
 6
 7    def resolve_all_books(self, info):
 8        return Book.objects.select_related('author', 'category').prefetch_related('tags')
 9
10    def resolve_books_by_category(self, info, category_id):
11        return Book.objects.filter(category_id=category_id).select_related('author')
12
13# Использование DataLoader для N+1 проблем
14from promise import Promise
15from promise.dataloader import DataLoader
16
17class BookLoader(DataLoader):
18    def batch_load_fn(self, keys):
19        books = Book.objects.filter(id__in=keys)
20        book_map = {book.id: book for book in books}
21        return [book_map.get(key) for key in keys]

Лучшие практики

  • Используй DataLoader для решения проблемы N+1 запросов
  • Ограничивай глубину запросов для предотвращения сложных запросов
  • Кэшируй часто запрашиваемые данные с помощью Redis
  • Валидируй входные данные на уровне схемы
  • Используй фрагменты для переиспользования полей
  • Логируй GraphQL запросы для отладки
  • Ограничивай сложность запросов с помощью middleware

FAQ

Q: Когда использовать GraphQL вместо REST?
A: Когда нужна гибкость в запросах данных, сложные связи между объектами, мобильные приложения или когда ты хочешь избежать over-fetching данных.

Q: Как обрабатывать файлы в GraphQL?
A: Используй специальные типы для загрузки файлов или обрабатывай их через REST endpoints, а в GraphQL передавай только ссылки.

Q: Можно ли использовать GraphQL с существующим REST API?
A: Да, можно постепенно мигрировать или использовать оба подхода параллельно, создавая GraphQL wrapper над существующими REST endpoints.

Q: Как обеспечить безопасность GraphQL API?
A: Используй аутентификацию, авторизацию, валидацию входных данных, ограничение глубины запросов и rate limiting.

Q: Как отлаживать GraphQL запросы?
A: Используй GraphiQL интерфейс, логируй запросы, используй инструменты типа Apollo Studio или GraphQL Playground.