GraphQL API в Django
GraphQL позволяет клиентам запрашивать точно те данные, которые им нужны, избегая проблем over-fetching и under-fetching, характерных для REST API.
Установка и настройка Graphene
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.