Implementación de GraphQL en Python

Ya vimos en el blog una introducción a GraphQL y cómo crear nuestro propio GraphQL Server en Java. Pero, para los amantes de hacer “magia” con 3 líneas de código, mostraremos cómo crear nuestro propio GraphQL Server en Python y si podemos alcanzar el “Uno para todos y todos para uno”.

Para ello, trabajaremos dos ejemplos con los frameworks para desarrollo web más populares actualmente: Django y Flask.

En todos ellos, utilizaremos la librería para Python Graphene, que justamente hace poco ha publicado su versión 2.0.

GraphQL + Django

El ejemplo que vamos a ver a continuación, puedes descargarlo aquí.

Stack

  • Django 1.11.5: este código ha sido implementado en Django. Es bastante todoterreno, capaz de aplicar una implementación sencilla a la mayoría de casuísticas que pueden encontrarse en un desarrollo web. Pero cuando un proyecto plantea casos muy específicos ni busques en Stackoverflow, porque salirse de los patrones que “impone” Django puede hacerte sudar y llorar.

¡Manos a la obra!

Una vez ejecutemos nuestro código (ver el repositorio para ver cómo) podremos empezar a realizar consultas con GraphiQL. Como en los ejemplos anteriores en Java, podemos realizar múltiples consultas con una petición.

Bien, hasta aquí nuestro servidor es igual que en las otras implementaciones, pero ¿qué está pasando por detrás?

En nuestro caso, hemos emulado los diferentes orígenes de datos con dos bases de datos de SQLite. Si hablamos de entornos productivos, la recomendación sería:

DATABASES = {
   'default': {
       'ENGINE': 'django.db.backends.sqlite3',
       'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
   },
   'brands_ddbb': {
       'ENGINE': 'django.db.backends.sqlite3',
       'NAME': os.path.join(BASE_DIR, 'db.sqlite3_brands'),
   }
}

Aprovechando las propias funcionalidades de Django, delegamos en su ORM la resolución de las queries contra diferentes orígenes. Por ejemplo, cuando recuperamos un objeto con todas sus relaciones, tal que:

{
    "data": {
        "car": {
            "color": "Red",
            "id": 1,
            "model": {
                "brand": {
                    "id": "1",
                    "name": "Seat"
                },
                "id": "4",
                "name": "Alhambra"
            }
        }
    }
}

Internamente se ataca a dos tablas, cada una en una base de datos distinta:

Esto es, gracias a un resolver definido como “resolve_car” en nuestro schema.py:

def resolve_car(self, info, **kwargs):
   id = kwargs.get('id')
   color = kwargs.get('color')
   if id is not None:
       return CarModel.objects.get(pk=id)

Si analizamos cuando recuperamos muchos objetos, como suele pasar en este tipo de implementaciones, vemos que nos encontramos con 1+N Queries Problem. Es decir, tiene que realizar una query adicional por cada N objetos que recuperamos.

Pero con los propios recursos de Django, como prefetch related, a pesar de las múltiples bases de datos, podemos solucionar el problema.

def resolve_cars(self, info, **kwargs):
   return CarModel.objects.all().prefetch_related('model', 'model__brand')

¡OJO! Django 1.x permite atacar múltiples bases de datos pero no es oro todo lo que reluce. En este caso, para poder insertar las relaciones entre la tabla “models”, como tiene una dependencia “brands” que pertenece a otro origen, hemos tenido que lanzar INSERTS y UPDATES a mano porque este framework no lo permite directamente.

Si quisiéramos añadir una nueva mutación que fuese CreateModel, nos encontraríamos con un problema.

Como conclusión en esta implementación en Django, podemos concluir:

Pros:

  • Implementación rápida. Si nosotros definimos el esquema, podemos tener un GraphQL server operativo en cuestión de horas.
  • Soluciones simples al problema de N+1 en bases de datos relacionales.

Contras:

  • El ORM, como dice la propia definición, está enfocado a bases de datos relacionales, por lo que juntarlo con una NoSQL puede ser un quebradero de cabeza.
  • Al igual que pasa en otras implementaciones, por ejemplo con Django Rest Framework, el modelo condiciona a la salida de la API siendo complicado y en muchos casos hay que recurrir a “apaños” para desacoplarlos rompiendo totalmente con la filosofía de GraphQL de separar el origen de los datos de nuestro esquema.

Podríamos decir entonces que el lema en este caso sería “Todos para uno y uno para todos… si todos cumplen las reglas de uno”.

Para que nuestro GraphQL server, gracias a sus resolvers, nos permita definir el origen de cada uno de nuestros objetos, lo veremos mejor en el siguiente ejemplos con Flask.

GraphQL + Flask

Puedes descargarte y probar este ejemplo desde este enlace.

Stack

  • Flask 0.12.2: framework muy orientado a microservicios.
  • Flask-GraphQL 1.4.1: de los creadores de Graphene, nos permite tener GrapiQL integrado en minutos.
  • SQLAlchemy 1.1.14: para mi gusto, el ORM más potente que existe para Python. Con una curva de aprendizaje más alta que el ORM de Django, pero mucho más potente y versátil.
  • Graphene-sqlalchemy 1.1.1: también de los mismos creadores de Graphene, nos permite integrar SQLAlchemy con Graphene, aunque nos obliga a caer en un error parecido a Django, haciendo que el modelo condicione el Schema de GraphQL.
  • PyMongo 3.5.1: librería para usar nuestra MongoDB desde Python.

En este caso, nuestro proyecto tiene una estructura muy parecida (con la salvedad que gracias a la simpleza de Flask, todo nuestro código no pasa de 150 líneas).

Como vemos a continuación, en nuestro esquema hemos utilizado dos implementaciones diferentes.

La primera, la definimos con la clase más “pura” de Graphene para recuperar nuestros datos contra una MongoDB, sin apoyarnos en librerías de terceros.

# schema.py
class Brand(graphene.ObjectType):
   name = graphene.String()

   class Meta:
       interfaces = (relay.Node,)

   @classmethod
   def get_node(cls, info, id):
       return get_brand(id)



# models.py
def get_brand(id):
   from schema import Brand
   result = brands.find_one({"_id": bson.ObjectId(str(id))})
   brand = Brand(id=str(result["_id"]), name=str(result["name"]))
   return brand

En este ejemplo vemos muy claramente la forma de trabajar con GraphQL, definimos nuestros datos y la respuesta, sin importar el origen, desacoplando de tal manera que nos da igual si esos datos vienen de una MongoDB o una API REST.

El segundo, nos apoyamos en graphene-sqlalchemy para relacionar el modelo de base de datos con nuestro esquema.

#schema.py
class Model(SQLAlchemyObjectType):
   brand = graphene.Field(Brand)

   class Meta:
       model = ModelModel
       interfaces = (relay.Node,)

   def resolve_brand(self, info, *args, **kwargs):
       return get_brand(self.brand_id)


class Car(SQLAlchemyObjectType):
   class Meta:
       model = CarModel
       interfaces = (relay.Node,)

Nos permite más nivel de desacoplamiento que en Django, pero seguimos atados. En este caso el lema sí sería “Todos para uno y uno para todos… pero uno no puede hacer que algo ocurra antes de que sea su hora”.

Como conclusión final de Python y Graphene:

Pros:

  • Como pasa en la mayoría de proyectos Python, con muy pocas líneas podemos crear recursos útiles en poco tiempo.
  • Si no usamos librerías que unan el ORM con GraphQL, permite mantener un buen nivel de desacoplamiento fácilmente mantenible.

Contras:

  • Para lo que suele ser habitual en Python, hay muy muy poca documentación, recursos y comunidad que avance en el tema.
  • Graphene trata de imitar a otras implementaciones, pero está a medio camino de ser una librería completa, siendo poco mantenida y con escasa documentación. Solo hay que ver las “aberraciones” que hay que hacer para definir un objeto recurriendo a importaciones dentro de una función para no caer en Cyclic Imports.Del mismo modo, no puedes definir esquemas que varios objetos se llamen entre sí. Por ejemplo, hemos sido incapaces de que si un Objeto tipo “Brand” muestra todos sus objetos tipo “Model” que a la vez un objeto “Model” muestre su “Brand” padre.
  • Se echa en falta una tan potente como GraphQL-apigen para Java, donde nos permite seguir el patrón “Schema First”. Pero animo a cualquier desarrollador interesado en la materia que intente crear su propia librería ;)

Developer, SyAdmin, QA, DevOps, Fullstack, Hacker... ¡qué importa la especialización! Mientras el puzzle suponga un reto, todo lo demás es superficial. Apasionado de la informática desde que tengo uso de conciencia y desde que en 2010 unos locos decidieron empezar a pagarme por hacer lo que me gusta las 24 horas del día

Ver toda la actividad de Alberto Vara

Escribe un comentario