Рубрики
Без рубрики

Построение API GraphQL с помощью Django

Автор оригинала: Marcus Sanatan.

Построение API GraphQL с помощью Django

Вступление

Веб-API-это движки, которые сегодня питают большинство наших приложений. В течение многих лет REST был доминирующей архитектурой для API, но в этой статье мы рассмотрим GraphQL .

С помощью REST API вы обычно создаете URL-адреса для каждого доступного объекта данных. Допустим, мы создаем REST API для фильмов – у нас будут URL-адреса для самих фильмов, актеров, наград, режиссеров, продюсеров… он уже становится громоздким! Это может означать множество запросов на один пакет связанных данных. Представьте, что вы были пользователем маломощного мобильного телефона с медленным подключением к Интернету, эта ситуация не идеальна.

GraphQL-это не архитектура API, как REST, это язык, который позволяет нам гораздо проще обмениваться связанными данными. Мы будем использовать его для разработки API для фильмов. Затем мы рассмотрим, как библиотека Graphene позволяет нам создавать API в Python, создавая API movie с помощью 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 s, или мы можем получить их список без фильтрации.

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

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

Мутации зависят от двух вещей:

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

Тип Мутация объединяет все это вместе:

type Mutation {
  createActor(input: ActorInput) : ActorPayload
  createMovie(input: MovieInput) : MoviePayload
  updateActor(id: ID!, input: ActorInput) : ActorPayload
  updateMovie(id: ID!, input: MovieInput) : MoviePayload
}

Для мутатора create Actor требуется объект Actor Input , для которого требуется имя актора.

Для update Actor mutator требуется ID обновляемого актора, а также обновленная информация.

То же самое следует и за фильмом “Мутаторы”.

Примечание : Хотя полезная нагрузка Actor и полезная нагрузка Movie не являются необходимыми для успешной мутации, 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

Примечание : Чтобы выйти из виртуальной среды и использовать обычную оболочку, введите деактивировать . Вы должны сделать это в конце урока.

Установка и настройка 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.

Теперь мы можем зарегистрировать наше приложение movies в рамках проекта. Идите в 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)

Создание нашей схемы с графеном

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

В нашей папке приложения movies создайте новый файл schema.py file и давайте определим наши типы 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 возвращают одно значение типа Actor и Movie Type соответственно, и оба требуют идентификатора, который является целым числом.

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

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

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

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

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

Вспомните сигнатуру для мутации create Actor , когда мы разрабатывали нашу схему:

createActor(input: ActorInput) : ActorPayload
  • Имя нашего класса соответствует имени запроса Graphql.
  • Внутренние Аргументы свойства класса соответствуют входным аргументам для мутатора.
  • Свойства ok и actor составляют полезную нагрузку Actor .

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

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

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

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

# 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 мы включили свойство actor в наш класс Movie и задали отношение “многие ко многим” между ними. Это позволяет нам получить все свойства типа Actor , связанные с данными фильма.

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

Запись Мутаций

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

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

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

Также обратите внимание, как возвращаемые значения ok и actor сопоставляются со свойствами класса мутации 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
    }
  }
}

Вот, все исправлено!

Общение по ПОЧТЕ

GraphiQL очень полезен во время разработки, но это стандартная практика, чтобы отключить это представление в производстве, так как это может позволить внешнему разработчику слишком много понимания API.

Чтобы отключить GraphiQL, просто отредактируйте django_graphql_movies/urls.py такой, что path('graphql/',)), становится path('graphql/',)), .

Приложение, взаимодействующее с вашим 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 (Cross-Site Request Forgery) – она имеет меры для предотвращения выполнения потенциально вредоносных действий неверно аутентифицированными пользователями сайта.

Хотя это полезная защита, она предотвратит взаимодействие внешних приложений с 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 для создания API GraphQL. Мы реализовали схему фильма, которую разработали ранее, и протестировали ее с помощью запросов GraphQL через GraphiQL и стандартного POST-запроса.

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