Создание GraphQL API с помощью Django в Python
На протяжении многих лет REST был доминирующей архитектурой для API, но в этой статье мы рассмотрим GraphQL.
С помощью REST API вы обычно создаете URL-адреса для каждого доступного объекта данных. Допустим, мы создаем REST API для фильмов – у нас будут URL-адреса для самих фильмов, актеров, наград, режиссеров, продюсеров … это уже становится громоздким! Это может означать множество запросов на один пакет связанных данных. Представьте, что вы пользуетесь мобильным телефоном с низким энергопотреблением и медленным интернет-соединением. Эта ситуация не идеальна.
GraphQL – это не архитектура API, как REST, это язык, который позволяет нам гораздо проще обмениваться связанными данными. Мы будем использовать его для разработки API для фильмов. После этого мы рассмотрим, как библиотека Graphene позволяет нам делать API на Python, создавая API фильмов с помощью Django.
Что такое GraphQL?
Первоначально созданный Facebook, но теперь разработанный в рамках GraphQL Foundation, GraphQL – это язык запросов и среда выполнения сервера, которая позволяет нам извлекать данные и манипулировать ими.
Мы используем строго типизированную систему GraphQL для определения данных, которые должны быть доступны для API. Затем мы создаем схему для API – набор разрешенных запросов для получения и изменения данных.
Разработка схемы
Типы описывают тип данных, доступных в API. Уже предоставлены примитивные типы, которые мы можем использовать, но мы также можем определить наши собственные пользовательские типы.
Рассмотрим следующие типы для актеров и фильмов:
type Actor {
id: ID!
name: String!
}
type Movie {
id: ID!
title: String!
actors: [Actor]
year: Int!
}
Тип идентификатора сообщает нам, что поле является уникальным идентификатором для этого типа данных. Если идентификатор не является строкой, для работы типа требуется способ сериализации в строку.
Примечание. Восклицательный знак означает, что это поле обязательно для заполнения.
Вы также можете заметить, что в Movie мы используем как примитивные типы, такие как String и Int, так и наш пользовательский тип Actor. Если мы хотим, чтобы поле содержало список типов, мы заключаем его в квадратные скобки – [Актер].
Создание запросов
В запросе указывается, какие данные можно получить и что требуется для их получения:
type Query {
actor(id: ID!): Actor
movie(id: ID!): Movie
actors: [Actor]
movies: [Movie]
}
Этот тип запроса позволяет нам получить данные об актере и фильме, указав их идентификаторы, или мы можем получить их список без фильтрации.
Создание Mutations
Mutations описывает, какие операции могут быть выполнены для изменения данных на сервере.
Mutations основаны на двух вещах:
- Входные данные – специальные типы используются только в качестве аргументов, когда мы хотим передать весь объект вместо отдельных полей.
- Полезные нагрузки – обычные типы, но по соглашению мы используем их в качестве выходных данных, чтобы мы могли легко расширять их по мере развития API.
Первое, что мы делаем, это создаем типы ввода:
input ActorInput {
id: ID
name: String!
}
input MovieInput {
id: ID
title: String
actors: [ActorInput]
year: Int
}
А затем мы создаем типы полезной нагрузки:
type ActorPayload {
ok: Boolean
actor: Actor
}
type MoviePayload {
ok: Boolean
movie: Movie
}
Обратите внимание на поле ok. Типы полезной нагрузки обычно включают метаданные, такие как поле состояния или ошибки.
Тип Mutation объединяет все вместе:
type Mutation {
createActor(input: ActorInput) : ActorPayload
createMovie(input: MovieInput) : MoviePayload
updateActor(id: ID!, input: ActorInput) : ActorPayload
updateMovie(id: ID!, input: MovieInput) : MoviePayload
}
Мутатору createActor нужен объект ActorInput, который требует имени актера.
Для мутатора updateActor требуется идентификатор обновляемого актера, а также обновленная информация.
То же самое и с мутаторами из фильмов.
Примечание. Хотя ActorPayload и MoviePayload не являются необходимыми для успешной мутации, для API рекомендуется предоставлять обратную связь при обработке действия.
Определение схемы
Наконец, мы сопоставляем созданные запросы со схемой:
schema {
query: Query
mutation: Mutation
}
Использование библиотеки
GraphQL не зависит от платформы, можно создать сервер GraphQL с различными языками программирования (Java, PHP, Go), фреймворками (Node.js, Symfony, Rails) или такими платформами, как Apollo.
С Graphene нам не нужно использовать синтаксис GraphQL для создания схемы, мы используем только Python. Эта библиотека с открытым исходным кодом также интегрирована с Django, поэтому мы можем создавать схемы, ссылаясь на модели нашего приложения.
Настройка приложения
Считается лучшей практикой создавать виртуальные среды для проектов Django. Начиная с Python 3.6, модуль venv был включен для создания виртуальных сред и управления ими.
Используя терминал, войдите в свое рабочее пространство и создайте следующую папку:
$ mkdir django_graphql_movies $ cd django_graphql_movies/
Теперь создайте виртуальную среду:
$ python3 -m venv env
Вы должны увидеть новую папку env в своем каталоге. Нам нужно активировать виртуальную среду, чтобы при установке пакетов Python они были доступны только для этого проекта, а не для всей системы:
$ . env/bin/activate
Примечание. Чтобы выйти из виртуальной среды и использовать обычную оболочку, введите deactivate. Вы должны сделать это в конце урока.
Установка и настройка Django и Graphene
Находясь в нашей виртуальной среде, мы используем pip для установки Django и библиотеки Graphene:
$ pip install Django $ pip install graphene_django
Затем мы создаем наш проект Django:
$ django-admin.py startproject django_graphql_movies .
Проект Django может состоять из множества приложений – это повторно используемые компоненты в рамках проекта, и лучше всего создавать наш проект с их помощью. Создадим приложение для наших фильмов:
$ cd django_graphql_movies/ $ django-admin.py startapp movies
Прежде чем мы будем работать над нашим приложением или запускать его, мы синхронизируем наши базы данных:
# First return to the project's directory $ cd .. # And then run the migrate command $ python manage.py migrate
Создание модели
Модели Django описывают структуру базы данных нашего проекта. Каждая модель представляет собой класс Python, который обычно сопоставляется с таблицей базы данных. Свойства класса сопоставляются со столбцами базы данных.
Введите следующий код в django_graphql_movies/movies/models.py:
from django.db import models
class Actor(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Meta:
ordering = ('name',)
class Movie(models.Model):
title = models.CharField(max_length=100)
actors = models.ManyToManyField(Actor)
year = models.IntegerField()
def __str__(self):
return self.title
class Meta:
ordering = ('title',)
Как и в случае со схемой GraphQL, модель «Актер» имеет имя, тогда как модель «Фильм» имеет заголовок, отношение «многие ко многим» с актерами и год. Идентификаторы автоматически генерируются для нас Django.
Теперь мы можем зарегистрировать наше приложение для фильмов в проекте. Перейдите в django_graphql_movies/settings.py и измените INSTALLED_APPS на следующее:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_graphql_movies.movies',
]
Обязательно перенесите свою базу данных, чтобы синхронизировать ее с изменениями нашего кода:
$ python manage.py makemigrations $ python manage.py migrate
Загрузка тестовых данных
После создания нашего API мы захотим иметь возможность выполнять запросы, чтобы проверить, работает ли он. Давайте загрузим некоторые данные в нашу базу данных, сохраним следующий JSON, как movies.json в корневом каталоге вашего проекта:
[
{
"model": "movies.actor",
"pk": 1,
"fields": {
"name": "Michael B. Jordan"
}
},
{
"model": "movies.actor",
"pk": 2,
"fields": {
"name": "Sylvester Stallone"
}
},
{
"model": "movies.movie",
"pk": 1,
"fields": {
"title": "Creed",
"actors": [1, 2],
"year": "2015"
}
}
]
И выполните следующую команду, чтобы загрузить тестовые данные:
$ python manage.py loaddata movies.json
В терминале вы должны увидеть следующий вывод:
Installed 3 object(s) from 1 fixture(s)
Создание схемы
В папке приложения с фильмами создайте новый файл schema.py и давайте определим наши типы GraphQL:
import graphene
from graphene_django.types import DjangoObjectType, ObjectType
from django_graphql_movies.movies.models import Actor, Movie
# Create a GraphQL type for the actor model
class ActorType(DjangoObjectType):
class Meta:
model = Actor
# Create a GraphQL type for the movie model
class MovieType(DjangoObjectType):
class Meta:
model = Movie
С помощью Graphene, чтобы создать тип GraphQL, мы просто указываем, какая модель Django имеет свойства, которые нам нужны в API.
В том же файле добавьте следующий код для создания типа запроса:
# Create a Query type
class Query(ObjectType):
actor = graphene.Field(ActorType, id=graphene.Int())
movie = graphene.Field(MovieType, id=graphene.Int())
actors = graphene.List(ActorType)
movies= graphene.List(MovieType)
def resolve_actor(self, info, **kwargs):
id = kwargs.get('id')
if id is not None:
return Actor.objects.get(pk=id)
return None
def resolve_movie(self, info, **kwargs):
id = kwargs.get('id')
if id is not None:
return Movie.objects.get(pk=id)
return None
def resolve_actors(self, info, **kwargs):
return Actor.objects.all()
def resolve_movies(self, info, **kwargs):
return Movie.objects.all()
Каждое свойство класса Query соответствует запросу GraphQL:
- Свойства актера и фильма возвращают одно значение ActorType и MovieType соответственно, и для обоих требуется целочисленный идентификатор.
- Свойства актеров и фильмов возвращают список своих соответствующих типов.
Четыре метода, которые мы создали в классе Query, называются преобразователями. Решатели связывают запросы в схеме с фактическими действиями, выполняемыми базой данных. Как обычно в Django, мы взаимодействуем с нашей базой данных через модели.
Рассмотрим функцию resolve_actor. Мы извлекаем идентификатор из параметров запроса и возвращаем actor из нашей базы данных с этим идентификатором в качестве его первичного ключа. Функция resolve_actors просто получает всех участников в базе данных и возвращает их в виде списка.
Создание mutation
При разработке схемы мы сначала создали специальные типы ввода для mutation. Сделаем то же самое с Graphene, добавим это в schema.py:
# Create Input Object Types
class ActorInput(graphene.InputObjectType):
id = graphene.ID()
name = graphene.String()
class MovieInput(graphene.InputObjectType):
id = graphene.ID()
title = graphene.String()
actors = graphene.List(ActorInput)
year = graphene.Int()
Это простые классы, которые определяют, какие поля можно использовать для изменения данных в API.
Создание mutation требует немного больше работы, чем создание запросов. Добавим mutation для actors:
# Create mutations for actors
class CreateActor(graphene.Mutation):
class Arguments:
input = ActorInput(required=True)
ok = graphene.Boolean()
actor = graphene.Field(ActorType)
@staticmethod
def mutate(root, info, input=None):
ok = True
actor_instance = Actor(name=input.name)
actor_instance.save()
return CreateActor(ok=ok, actor=actor_instance)
class UpdateActor(graphene.Mutation):
class Arguments:
id = graphene.Int(required=True)
input = ActorInput(required=True)
ok = graphene.Boolean()
actor = graphene.Field(ActorType)
@staticmethod
def mutate(root, info, id, input=None):
ok = False
actor_instance = Actor.objects.get(pk=id)
if actor_instance:
ok = True
actor_instance.name = input.name
actor_instance.save()
return UpdateActor(ok=ok, actor=actor_instance)
return UpdateActor(ok=ok, actor=None)
Вспомните сигнатуру createActor, когда мы разрабатывали нашу схему:
createActor(input: ActorInput) : ActorPayload
- Имя нашего класса соответствует имени запроса GraphQL.
- Внутренние свойства класса Arguments соответствуют входным аргументам мутатора.
- Свойства ok и актер составляют ActorPayload.
Главное, что нужно знать при написании метода mutation, – это то, что вы сохраняете данные в модели Django:
- Мы берем имя из входного объекта и создаем новый объект Actor.
- Мы вызываем функцию сохранения, чтобы обновить нашу базу данных, и возвращаем полезные данные пользователю.
Класс UpdateActor имеет аналогичную настройку с дополнительной логикой для извлечения обновляемого Actor и изменения его свойств перед сохранением.
Теперь добавим mutation для фильмов:
# Create mutations for movies
class CreateMovie(graphene.Mutation):
class Arguments:
input = MovieInput(required=True)
ok = graphene.Boolean()
movie = graphene.Field(MovieType)
@staticmethod
def mutate(root, info, input=None):
ok = True
actors = []
for actor_input in input.actors:
actor = Actor.objects.get(pk=actor_input.id)
if actor is None:
return CreateMovie(ok=False, movie=None)
actors.append(actor)
movie_instance = Movie(
title=input.title,
year=input.year
)
movie_instance.save()
movie_instance.actors.set(actors)
return CreateMovie(ok=ok, movie=movie_instance)
class UpdateMovie(graphene.Mutation):
class Arguments:
id = graphene.Int(required=True)
input = MovieInput(required=True)
ok = graphene.Boolean()
movie = graphene.Field(MovieType)
@staticmethod
def mutate(root, info, id, input=None):
ok = False
movie_instance = Movie.objects.get(pk=id)
if movie_instance:
ok = True
actors = []
for actor_input in input.actors:
actor = Actor.objects.get(pk=actor_input.id)
if actor is None:
return UpdateMovie(ok=False, movie=None)
actors.append(actor)
movie_instance.title=input.title
movie_instance.year=input.year
movie_instance.save()
movie_instance.actors.set(actors)
return UpdateMovie(ok=ok, movie=movie_instance)
return UpdateMovie(ok=ok, movie=None)
Поскольку фильмы ссылаются на актеров, мы должны получить данные об актерах из базы данных перед сохранением. Цикл for сначала проверяет, что субъекты, предоставленные пользователем, действительно находятся в базе данных, если нет, он возвращается без сохранения каких-либо данных.
При работе с отношениями «многие ко многим» в Django мы можем сохранять связанные данные только после сохранения нашего объекта.
Вот почему мы сохраняем наш фильм с помощью movie_instance.save() перед тем, как назначить ему актеров с помощью movie_instance.actors.set.
Чтобы завершить, мы создаем Тип mutation:
class Mutation(graphene.ObjectType):
create_actor = CreateActor.Field()
update_actor = UpdateActor.Field()
create_movie = CreateMovie.Field()
update_movie = UpdateMovie.Field()
Создание схемы
Как и раньше, когда мы разрабатывали нашу схему, мы сопоставляем запросы с API нашего приложения. Добавьте это в конец schema.py:
schema = graphene.Schema(query=Query, mutation=Mutation)
Регистрация схемы в проекте
Чтобы наш API работал, нам нужно сделать схему доступной для всего проекта.
Создайте новый файл schema.py в django_graphql_movies/ и добавьте следующее:
import graphene
import django_graphql_movies.movies.schema
class Query(django_graphql_movies.movies.schema.Query, graphene.ObjectType):
# This class will inherit from multiple Queries
# as we begin to add more apps to our project
pass
class Mutation(django_graphql_movies.movies.schema.Mutation, graphene.ObjectType):
# This class will inherit from multiple Queries
# as we begin to add more apps to our project
pass
schema = graphene.Schema(query=Query, mutation=Mutation)
Отсюда мы можем зарегистрировать graph и указать ему использовать нашу схему.
Откройте django_graphql_movies/settings.py и добавьте graphene_django в качестве первого элемента в INSTALLED_APPS.
В том же файле добавьте следующий код на пару новых строк ниже INSTALLED_APPS:
GRAPHENE = {
'SCHEMA': 'django_graphql_movies.schema.schema'
}
Доступ к API GraphQL осуществляется через одну конечную точку и graphql. Нам нужно зарегистрировать этот маршрут или, скорее, просмотр в Django.
Откройте django_graphql_movies/urls.py и измените содержимое файла на:
from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
from django_graphql_movies.schema import schema
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', GraphQLView.as_view(graphiql=True)),
]
Тестирование API
Чтобы протестировать наш API, запустим проект, а затем перейдем к конечной точке GraphQL. В типе терминала:
$ python manage.py runserver
Как только ваш сервер будет запущен, вы столкнетесь с GraphiQL – встроенной IDE для выполнения ваших запросов.
Написание запросов
Для нашего первого запроса давайте получим всех актеров в нашей базе данных. На левой верхней панели введите следующее:
query getActors {
actors {
id
name
}
}
Это формат запроса в GraphQL. Мы начинаем с ключевого слова запроса, за которым следует необязательное имя. Рекомендуется давать запросам имена, поскольку это помогает при ведении журнала и отладке. GraphQL позволяет нам указывать и те поля, которые нам нужны – мы выбрали id и name.
Несмотря на то, что у нас есть только один фильм в наших тестовых данных, давайте попробуем запрос фильма и обнаружим еще одну замечательную функцию GraphQL:
query getMovie {
movie(id: 1) {
id
title
actors {
id
name
}
}
}
Для запроса фильма требуется идентификатор, поэтому мы указываем его в скобках. Интересный момент связан с полем актеров. В нашей модели Django мы включили свойство акторов в наш класс Movie и указали между ними отношение «многие ко многим». Это позволяет нам получить все свойства типа Actor, связанные с данными фильма.
Этот графоподобный обход данных – основная причина, по которой GraphQL считается мощной и захватывающей технологией.
Написание mutation
Мутации следуют тому же стилю, что и запросы. Добавим актера в нашу базу данных:
mutation createActor {
createActor(input: {
name: "Tom Hanks"
}) {
ok
actor {
id
name
}
}
}
Обратите внимание, как входной параметр соответствует входным свойствам классов Arguments, которые мы создали ранее.
Также обратите внимание, как возвращаемые значения ok и актер отображаются в свойствах класса CreateActor.
Теперь мы можем добавить фильм, в котором снимался Том Хэнкс:
mutation createMovie {
createMovie(input: {
title: "Cast Away",
actors: [
{
id: 3
}
]
year: 1999
}) {
ok
movie{
id
title
actors {
id
name
}
year
}
}
}
К сожалению, мы просто ошиблись. «Изгнанный» вышел в 2000 году!
Давайте запустим запрос на обновление, чтобы исправить это:
mutation updateMovie {
updateMovie(id: 2, input: {
title: "Cast Away",
actors: [
{
id: 3
}
]
year: 2000
}) {
ok
movie{
id
title
actors {
id
name
}
year
}
}
}
Там все исправлено!
Общение через POST
GraphiQL очень полезен во время разработки, но стандартной практикой является отключение этого представления в производственной среде, так как это может позволить внешнему разработчику слишком хорошо разбираться в API.
Чтобы отключить GraphiQL, просто отредактируйте django_graphql_movies/urls.py так, чтобы путь (‘graphql/’, GraphQLView.as_view (graphiql = True)) стал путем (‘graphql /’, GraphQLView.as_view (graphiql = False)).
Приложение, взаимодействующее с вашим API, будет отправлять запросы POST в конечную точку и graphql. Прежде чем мы сможем отправлять запросы POST извне сайта Django, нам нужно изменить django_graphql_movies/urls.py:
from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
from django_graphql_movies.schema import schema
from django.views.decorators.csrf import csrf_exempt # New library
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]
Django имеет встроенную защиту CSRF (подделка межсайтовых запросов) – в нем есть меры по предотвращению выполнения потенциально вредоносных действий некорректно аутентифицированными пользователями сайта.
Хотя это полезная защита, она помешает внешним приложениям взаимодействовать с API. Вам следует рассмотреть другие формы аутентификации, если вы запускаете свое приложение в производство.
В вашем терминале введите следующее, чтобы получить всех актеров:
$ curl \
-X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{ actors { name } }" }' \
http://127.0.0.1:8000/graphql/
Вы должны получить:
{"data":{"actors":[{"name":"Michael B. Jordan"},{"name":"Sylvester Stallone"},{"name":"Tom Hanks"}]}}
Заключение
GraphQL – это строго типизированный язык запросов, который помогает создавать развивающиеся API. Мы разработали схему API для фильмов, создав необходимые типы и запросы, необходимые для получения и изменения данных.
С Graphene мы можем использовать Django для создания GraphQL API. Мы реализовали схему фильма, которую разработали ранее, и протестировали ее с помощью запросов GraphQL через GraphiQL и стандартного запроса POST.
Автор