Создание GraphQL API с помощью Django

Введение. Веб-API - это движок, на котором сегодня работает большинство наших приложений. На протяжении многих лет REST был доминирующей архитектурой для API, но в этой статье мы рассмотрим GraphQL [https://graphql.org/learn/]. С помощью REST API вы обычно создаете URL-адреса для каждого доступного объекта данных. Допустим, мы создаем REST API для фильмов - у нас будут URL-адреса для самих фильмов, актеров, наград, режиссеров, продюсеров ... это уже становится громоздким! Это может означать много запросов

Вступление

Веб-API - это движки, на которых сегодня работает большинство наших приложений. На протяжении многих лет 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! 
 } 

Тип ID сообщает нам, что поле является уникальным идентификатором для этого типа данных. Если ID не является строкой, для работы типа требуется способ сериализации в строку!

Примечание . Восклицательный знак означает, что это поле обязательно для заполнения .

Вы также можете заметить, что в Movie мы используем как примитивные типы, такие как String и Int так и наш пользовательский тип Actor

Если мы хотим, чтобы поле содержало список типов, мы заключаем его в квадратные скобки - [Actor] .

Создание запросов

В запросе указывается, какие данные можно получить и что требуется для их получения:

 type Query { 
 actor(id: ID!): Actor 
 movie(id: ID!): Movie 
 actors: [Actor] 
 movies: [Movie] 
 } 

Этот Query позволяет нам получить данные об Actor и Movie , указав их ID , или мы можем получить их список без фильтрации.

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

Мутация описывает, какие операции можно выполнять для изменения данных на сервере.

Мутации основаны на двух вещах:

  • Входные данные - специальные типы используются только в качестве аргументов при мутации, когда мы хотим передать весь объект, а не отдельные поля.
  • Полезные нагрузки - обычные типы, но по соглашению мы используем их в качестве выходных данных для мутации, чтобы мы могли легко расширять их по мере развития 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 ID обновляемого актера, а также обновленная информация.

То же самое и с мутаторами из фильмов.

Примечание . Хотя 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, Actor имеет имя, тогда как Movie имеет заголовок, отношение «многие ко многим» с актерами и год. Идентификаторы автоматически генерируются для нас 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.

В том же файле добавьте следующий код для создания типа Query

 # 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:

  • actor и movie возвращают одно значение ActorType и MovieType соответственно, и для обоих требуется целочисленный идентификатор.

  • actors и movies возвращают список своих соответствующих типов.

Четыре метода, которые мы создали в классе Query, называются преобразователями . Решатели связывают запросы в схеме с фактическими действиями, выполняемыми базой данных. Как обычно в Django, мы взаимодействуем с нашей базой данных через модели.

Рассмотрим функцию resolve_actor Мы получаем идентификатор из параметров запроса и возвращаем актера из нашей базы данных с этим идентификатором в качестве его первичного ключа. Функция resolve_actors просто получает всех участников в базе данных и возвращает их в виде списка.

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

При разработке схемы мы сначала создали специальные типы ввода для наших мутаций. Сделаем то же самое с 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.

Создание мутаций требует немного больше работы, чем создание запросов. Добавим мутации для актеров:

 # 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 и actor ActorPayload .

Главное, что нужно знать при написании mutation - это то, что вы сохраняете данные в модели Django:

  • Мы берем имя из входного объекта и создаем новый объект Actor
  • Мы вызываем save чтобы обновить нашу базу данных, и возвращаем полезные данные пользователю.

Класс UpdateActor имеет аналогичную настройку с дополнительной логикой для извлечения обновляемого актора и изменения его свойств перед сохранением.

Теперь добавим мутацию для фильмов:

 # 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(actors) .

Чтобы завершить наши мутации, мы создаем Тип Мутации:

 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) 

Отсюда мы можем зарегистрировать графен и указать ему использовать нашу схему.

Откройте 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 

Как только ваш сервер будет запущен, http://127.0.0.1:8000/graphql/ . Вы столкнетесь с GraphiQL - встроенной IDE для выполнения ваших запросов!

Написание запросов

Для нашего первого запроса давайте получим всех актеров в нашей базе данных. На левой верхней панели введите следующее:

 query getActors { 
 actors { 
 id 
 name 
 } 
 } 

Это формат запроса в GraphQL. Мы начинаем с query , за которым следует необязательное имя запроса. Рекомендуется давать запросам имена, поскольку это помогает при ведении журнала и отладке. GraphQL позволяет нам указывать и те поля, которые нам нужны - мы выбрали id и name .

Несмотря на то, что у нас есть только один фильм в наших тестовых данных, давайте попробуем movie и обнаружим еще одну замечательную функцию GraphQL:

 query getMovie { 
 movie(id: 1) { 
 id 
 title 
 actors { 
 id 
 name 
 } 
 } 
 } 

Для movie требуется идентификатор, поэтому мы указываем его в скобках. Интересный момент связан с полем actors В нашей модели Django мы включили actors в наш Movie и указали между ними отношение «многие ко многим». Это позволяет нам получить все свойства типа Actor , связанные с данными фильма.

Этот графоподобный обход данных - основная причина, по которой GraphQL считается мощной и захватывающей технологией!

Написание мутаций

Мутации следуют тому же стилю, что и запросы. Добавим актера в нашу базу данных:

 mutation createActor { 
 createActor(input: { 
 name: "Tom Hanks" 
 }) { 
 ok 
 actor { 
 id 
 name 
 } 
 } 
 } 

Обратите внимание, как input параметр соответствует input свойствам Arguments которые мы создали ранее.

Также обратите внимание, как ok и actor отображаются в свойствах CreateActor мутации 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 , чтобы path('graphql/', GraphQLView.as_view(graphiql=True)), стал path('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.

Если вы хотите увидеть исходный код всего приложения, вы можете найти его здесь .

comments powered by Disqus