Construyendo una API GraphQL con Django

C

Introducción

Las API web son los motores que impulsan la mayoría de nuestras aplicaciones en la actualidad. Durante muchos años, REST ha sido la arquitectura dominante para las API, pero en este artículo exploraremos GraphQL.

Con las API REST, generalmente crea URL para cada objeto de datos que es accesible. Digamos que estamos construyendo una API REST para películas: tendremos URL para las películas en sí, actores, premios, directores, productores … ¡ya se está volviendo difícil de manejar! Esto podría significar muchas solicitudes de un lote de datos relacionados. Imagina que eres el usuario de un teléfono móvil de baja potencia con una conexión a Internet lenta, esta situación no es ideal.

GraphQL no es una arquitectura API como REST, es un lenguaje que nos permite compartir datos relacionados de una manera mucho más fácil. Lo usaremos para diseñar una API para películas. Luego, veremos cómo la biblioteca Graphene nos permite construir API en Python al hacer una API de película con Django.

Que es GraphQL

Originalmente creado por Facebook pero ahora desarrollado bajo GraphQL Foundation, GraphQL es un lenguaje de consulta y tiempo de ejecución de servidor que nos permite recuperar y manipular datos.

Aprovechamos el sistema fuertemente tipado de GraphQL para definir los datos que queremos que estén disponibles para la API. Luego, creamos un esquema para la API: el conjunto de consultas permitidas para recuperar y modificar datos.

Diseñar un esquema de película

Creando Nuestros Tipos

Los tipos describen el tipo de datos que están disponibles en la API. Ya existen tipos primitivos proporcionados que podemos usar, pero también podemos definir nuestros propios tipos personalizados.

Considere los siguientes tipos de actores y películas:

type Actor {
  id: ID!
  name: String!
}

type Movie {
  id: ID!
  title: String!
  actors: [Actor]
  year: Int!
}

los ID type nos dice que el campo es el identificador único para ese tipo de datos. Si el ID no es una cadena, el tipo necesita una forma de serializarse en una cadena para que funcione.

Nota: El signo de exclamación significa que el campo es obligatorio.

También notarías que en Movie usamos ambos tipos primitivos como String y Int así como nuestra costumbre Actor tipo.

Si queremos que un campo contenga la lista del tipo, lo encerramos entre corchetes – [Actor].

Crear consultas

Una consulta especifica qué datos se pueden recuperar y qué se requiere para acceder a ellos:

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

Esta Query tipo nos permite obtener el Actor y Movie datos proporcionando su IDs, o podemos obtener una lista de ellos sin filtrar.

Creando mutaciones

Una mutación describe qué operaciones se pueden realizar para cambiar los datos en el servidor.

Las mutaciones se basan en dos cosas:

  • Entradas – tipos especiales que solo se utilizan como argumentos en una mutación cuando queremos pasar un objeto completo en lugar de campos individuales.
  • Cargas útiles – tipos regulares, pero por convención los usamos como salidas para una mutación para que podamos extenderlos fácilmente a medida que evoluciona la API.

Lo primero que hacemos es crear los tipos de entrada:

input ActorInput {
  id: ID
  name: String!
}

input MovieInput {
  id: ID
  title: String
  actors: [ActorInput]
  year: Int
}

Y luego creamos los tipos de carga útil:

type ActorPayload {
  ok: Boolean
  actor: Actor
}

type MoviePayload {
  ok: Boolean
  movie: Movie
}

Toma nota del ok campo, es común que los tipos de carga útil incluyan metadatos como un estado o un campo de error.

los Mutation type lo reúne todo:

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

los createActor el mutador necesita un ActorInput objeto, que requiere el nombre del actor.

los updateActor mutador requiere el ID del actor que se actualiza, así como la información actualizada.

Lo mismo ocurre con los mutantes de la película.

Nota: Mientras que la ActorPayload y MoviePayload no son necesarios para una mutación exitosa, es una buena práctica que las API proporcionen comentarios cuando procesan una acción.

Definiendo el esquema

Finalmente, asignamos las consultas y mutaciones que hemos creado al esquema:

schema {
  query: Query
  mutation: Mutation
}

Usando la biblioteca de grafeno

GraphQL es independiente de la plataforma, uno puede crear un servidor GraphQL con una variedad de lenguajes de programación (Java, PHP, Go), frameworks (Node.js, Symfony, Rails) o plataformas como Apollo.

Con Grafeno, no tenemos que usar la sintaxis de GraphQL para crear un esquema, ¡solo usamos Python! Esta biblioteca de código abierto también se ha integrado con Django para que podamos crear esquemas haciendo referencia a los modelos de nuestra aplicación.

Configuración de la aplicación

Ambientes virtuales

Se considera la mejor práctica para crear entornos virtuales para proyectos de Django. Desde Python 3.6, el venv Se ha incluido un módulo para crear y gestionar entornos virtuales.

Usando la terminal, ingrese a su espacio de trabajo y cree la siguiente carpeta:

$ mkdir django_graphql_movies
$ cd django_graphql_movies/

Ahora crea el entorno virtual:

$ python3 -m venv env

Deberías ver un nuevo env carpeta en su directorio. Necesitamos activar nuestro entorno virtual, para que cuando instalemos paquetes de Python solo estén disponibles para este proyecto y no para todo el sistema:

$ . env/bin/activate

Nota: Para salir del entorno virtual y utilizar su shell habitual, escriba deactivate. Debe hacer esto al final del tutorial.

Instalación y configuración de Django y Graphene

Mientras estamos en nuestro entorno virtual, usamos pip para instalar Django y la biblioteca Graphene:

$ pip install Django
$ pip install graphene_django

Luego creamos nuestro proyecto Django:

$ django-admin.py startproject django_graphql_movies .

Un proyecto de Django puede constar de muchas aplicaciones. Las aplicaciones son componentes reutilizables dentro de un proyecto, y es una buena práctica crear nuestro proyecto con ellas. Creemos una aplicación para nuestras películas:

$ cd django_graphql_movies/
$ django-admin.py startapp movies

Antes de trabajar en nuestra aplicación o ejecutarla, sincronizaremos nuestras bases de datos:

# First return to the project's directory
$ cd ..
# And then run the migrate command
$ python manage.py migrate

Creando un modelo

Los modelos de Django describen el diseño de la base de datos de nuestro proyecto. Cada modelo es una clase de Python que generalmente se asigna a una tabla de base de datos. Las propiedades de la clase se asignan a las columnas de la base de datos.

Escriba el siguiente código para 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',)

Al igual que con el esquema GraphQL, el Actor modelo tiene un nombre mientras que el Movie modelo tiene un título, una relación de muchos a muchos con los actores y un año. Django genera automáticamente los ID.

Ahora podemos registrar nuestra aplicación de películas dentro del proyecto. Ir al django_graphql_movies/settings.py y cambia el INSTALLED_APPS a lo siguiente:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_graphql_movies.movies',
]

Asegúrese de migrar su base de datos para mantenerla sincronizada con nuestros cambios de código:

$ python manage.py makemigrations
$ python manage.py migrate

Cargando datos de prueba

Después de que construyamos nuestra API, querremos poder realizar consultas para probar si funciona. Carguemos algunos datos en nuestra base de datos, guarde el siguiente JSON como movies.json en el directorio raíz de su proyecto:

[
  {
    "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"
    }
  }
]

Y ejecute el siguiente comando para cargar los datos de prueba:

$ python manage.py loaddata movies.json

Debería ver la siguiente salida en la terminal:

Installed 3 object(s) from 1 fixture(s)

Creando nuestro esquema con grafeno

Realización de consultas

En nuestra carpeta de aplicaciones de películas, cree una nueva schema.py file y definamos nuestros tipos 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

Con la ayuda de Graphene, para crear un tipo GraphQL simplemente especificamos qué modelo de Django tiene las propiedades que queremos en la API.

En el mismo archivo, agregue el siguiente código para crear el Query tipo:

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

Cada propiedad del Query clase corresponde a una consulta GraphQL:

  • los actor y movie propiedades devuelven un valor de ActorType y MovieType respectivamente, y ambos requieren un ID que sea un número entero.
  • los actors y movies las propiedades devuelven una lista de sus respectivos tipos.

Los cuatro métodos que creamos en la clase Query se llaman resolutores. Los solucionadores conectan las consultas en el esquema con las acciones reales realizadas por la base de datos. Como es estándar en Django, interactuamos con nuestra base de datos a través de modelos.

Considera el resolve_actor función. Recuperamos el ID de los parámetros de consulta y devolvemos el actor de nuestra base de datos con ese ID como clave principal. los resolve_actors La función simplemente obtiene todos los actores de la base de datos y los devuelve como una lista.

Haciendo mutaciones

Cuando diseñamos el esquema, primero creamos tipos de entrada especiales para nuestras mutaciones. Hagamos lo mismo con el grafeno, agregue esto a 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()

Son clases simples que definen qué campos se pueden usar para cambiar datos en la API.

La creación de mutaciones requiere un poco más de trabajo que la creación de consultas. Agreguemos las mutaciones para los actores:

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

Recuerde la firma del createActor mutación cuando diseñamos nuestro esquema:

createActor(input: ActorInput) : ActorPayload
  • El nombre de nuestra clase corresponde al nombre de la consulta de GraphQL.
  • El interior Arguments las propiedades de la clase corresponden a los argumentos de entrada para el mutador.
  • los ok y actor propiedades componen el ActorPayload.

La clave a saber al escribir un mutation El método es que está guardando los datos en el modelo Django:

  • Tomamos el nombre del objeto de entrada y creamos un nuevo Actor objeto.
  • Llamamos al save función para que nuestra base de datos se actualice y devuelva la carga útil al usuario.

los UpdateActor La clase tiene una configuración similar con lógica adicional para recuperar el actor que se está actualizando y cambiar sus propiedades antes de guardar.

Ahora agreguemos la mutación para películas:

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

Como las películas hacen referencia a los actores, tenemos que recuperar los datos de los actores de la base de datos antes de guardarlos. los for loop primero verifica que los actores proporcionados por el usuario estén efectivamente en la base de datos, si no, regresa sin guardar ningún dato.

Cuando trabajamos con relaciones de muchos a muchos en Django, solo podemos guardar datos relacionados después de que se guarde nuestro objeto.

Por eso guardamos nuestra película con movie_instance.save() antes de poner a los actores con movie_instance.actors.set(actors).

Para completar nuestras mutaciones, creamos el tipo de mutación:

class Mutation(graphene.ObjectType):
    create_actor = CreateActor.Field()
    update_actor = UpdateActor.Field()
    create_movie = CreateMovie.Field()
    update_movie = UpdateMovie.Field()

Haciendo el esquema

Como antes, cuando diseñamos nuestro esquema, asignamos las consultas y mutaciones a la API de nuestra aplicación. Agregue esto al final de schema.py:

schema = graphene.Schema(query=Query, mutation=Mutation)

Registro del esquema en el proyecto

Para que nuestra API funcione, necesitamos hacer que un esquema esté disponible para todo el proyecto.

Crear un nuevo schema.py presentar en django_graphql_movies/ y agregue lo siguiente:

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)

Desde aquí podemos registrar el grafeno y decirle que use nuestro esquema.

Abierto django_graphql_movies/settings.py y añadir 'graphene_django', como primer elemento de la INSTALLED_APPS.

En el mismo archivo, agregue el siguiente código un par de líneas nuevas debajo del INSTALLED_APPS:

GRAPHENE = {
    'SCHEMA': 'django_graphql_movies.schema.schema'
}

Las API de GraphQL se alcanzan a través de un punto final, /graphql. Necesitamos registrar esa ruta, o más bien vista, en Django.

Abierto django_graphql_movies/urls.py y cambie el contenido del archivo a:

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)),
]

Probando nuestra API

Para probar nuestra API, ejecutemos el proyecto y luego vayamos al punto final GraphQL. En el tipo de terminal:

$ python manage.py runserver

Una vez que su servidor esté funcionando, diríjase a http://127.0.0.1:8000/graphql/. Te encontrarás GraphiQL – ¡un IDE integrado para ejecutar sus consultas!

Escribir consultas

Para nuestra primera consulta, obtengamos todos los actores en nuestra base de datos. En el panel superior izquierdo, ingrese lo siguiente:

query getActors {
  actors {
    id
    name
  }
}

Este es el formato para una consulta en GraphQL. Comenzamos con el query palabra clave, seguida de un nombre opcional para la consulta. Es una buena práctica dar un nombre a las consultas, ya que ayuda con el registro y la depuración. GraphQL nos permite especificar los campos que queremos también – elegimos id y name.

Aunque solo tenemos una película en nuestros datos de prueba, probemos el movie consulta y descubre otra gran característica de GraphQL:

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

los movie La consulta requiere un ID, por lo que proporcionamos uno entre paréntesis. Lo interesante viene con el actors campo. En nuestro modelo de Django incluimos el actors propiedad en nuestra Movie class y especificó una relación de varios a varios entre ellos. Esto nos permite recuperar todas las propiedades de un Actor tipo que está relacionado con los datos de la película.

Este recorrido de datos en forma de gráfico es una de las principales razones por las que GraphQL se considera una tecnología poderosa y emocionante.

Escritura de mutaciones

Las mutaciones siguen un estilo similar a las consultas. Agreguemos un actor a nuestra base de datos:

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

Note como el input parámetro corresponde al input propiedades del Arguments clases que creamos anteriormente.

También observe cómo ok y actor los valores de retorno se asignan a las propiedades de clase del CreateActor mutación.

Ahora podemos agregar una película en la que actuó Tom Hanks:

mutation createMovie {
  createMovie(input: {
    title: "Cast Away",
    actors: [
      {
        id: 3
      }
    ]
    year: 1999
  }) {
    ok
    movie{
      id
      title
      actors {
        id
        name
      }
      year
    }
  }
}

Desafortunadamente, cometimos un error. ¡”Cast Away” salió en el año 2000!

Ejecutemos una consulta de actualización para solucionarlo:

mutation updateMovie {
  updateMovie(id: 2, input: {
    title: "Cast Away",
    actors: [
      {
        id: 3
      }
    ]
    year: 2000
  }) {
    ok
    movie{
      id
      title
      actors {
        id
        name
      }
      year
    }
  }
}

¡Ahí, todo arreglado!

Comunicación vía POST

GraphiQL es muy útil durante el desarrollo, pero es una práctica estándar deshabilitar esa vista en producción, ya que puede permitir que un desarrollador externo tenga demasiada información sobre la API.

Para deshabilitar GraphiQL, simplemente edite django_graphql_movies/urls.py tal que path('graphql/', GraphQLView.as_view(graphiql=True)), se convierte en path('graphql/', GraphQLView.as_view(graphiql=False)),.

Una aplicación que se comunica con su API enviaría solicitudes POST al /graphql punto final. Antes de que podamos realizar solicitudes POST desde fuera del sitio de Django, debemos cambiar 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 viene integrado con CSRF Protección (Falsificación de solicitudes entre sitios): tiene medidas para evitar que los usuarios autenticados incorrectamente del sitio realicen acciones potencialmente maliciosas.

Si bien esta es una protección útil, evitaría que las aplicaciones externas se comuniquen con la API. Debería considerar otras formas de autenticación si pone su aplicación en producción.

En su terminal ingrese lo siguiente para obtener todos los actores:

$ curl 
  -X POST 
  -H "Content-Type: application/json" 
  --data '{ "query": "{ actors { name } }" }' 
  http://127.0.0.1:8000/graphql/

Tu deberías recibir:

{"data":{"actors":[{"name":"Michael B. Jordan"},{"name":"Sylvester Stallone"},{"name":"Tom Hanks"}]}}

Conclusión

GraphQL es un lenguaje de consulta fuertemente tipado que ayuda a crear API evolutivas. Diseñamos un esquema de API para películas, creando los tipos, consultas y mutaciones necesarias para obtener y cambiar datos.

Con Graphene podemos usar Django para crear API GraphQL. Implementamos el esquema de película que diseñamos anteriormente y lo probamos usando consultas GraphQL a través de GraphiQL y una solicitud POST estándar.

Si desea ver el código fuente de la aplicación completa, puede encontrarlo aquí.

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad