5 min read

Introduction à GraphQL avec Python (partie 2/2)

Dans notre dernier article consacré à l'introduction à GraphQL avec Python, nous avons:

  • instancié un projet avec Django et Graphene
  • mis en place un modèle de données
  • exécuté nos premières requêtes pour récupérer nos interventions

Dans cette deuxième partie, nous allons pousser un peu plus loin la découverte en mettant en place un modèle de données plus complexe pour enfin créer nos propres entrées via GraphQL.

Un modèle de données plus complexe

Notre précédent modèle de données avait pour intérêt d'être simpliste pour illustrer les bases de la requête avec GraphQL. Il est désormais intéressant de pousser un plus la réflexion avec l'ajout de la gestion de tâches. Chaque tâche est composée d'un titre, est attaché à une intervention et à un statut (réalisé ou non). Désormais, chaque intervention est une collection de tâches.

from django.db import models


class Intervention(models.Model):
    title = models.CharField(max_length=180)
    description = models.TextField(blank=True)


class Task(models.Model):
    title = models.CharField(max_length=180)
    intervention = models.ForeignKey(
        Intervention,
        related_name='tasks',
        on_delete=models.CASCADE)
    is_done = models.BooleanField(default=False)

De la même façon que nous pouvions récupérer les propriétés d'une intervention, nous pouvons désormais effectuer des requêtes pour remonter les tâches associées.

Obtenir un enregistrement spécifique

Obtenir la liste des objets est désormais trivial pour nous mais qu'en est-il de la récupération d'un élément spécifique ? Imaginons que nous voulions récupérer l'intervention dont l'ID est 1 ou encore la tâche dont le nom est "formater le disque dur" ? ‌‌Mettons à jour le fichier schema.py de notre application:

import graphene
from graphene_django import DjangoObjectType

from .models import Intervention, Task


class InterventionType(DjangoObjectType):
    class Meta:
        model = Intervention


class TaskType(DjangoObjectType):
    class Meta:
        model = Task


class Query(graphene.ObjectType):
    # all interventions
    interventions = graphene.List(InterventionType)
    #specific intervention
    intervention = graphene.Field(InterventionType,
                              id=graphene.Int())
    #all tasks
    tasks = graphene.List(TaskType)
    #specific task
    task = graphene.Field(TaskType,
                          id=graphene.Int(),
                          title=graphene.String())

    def resolve_interventions(self, info, **kwargs):
        return Intervention.objects.all()

    def resolve_intervention(self, info, **kwargs):
        id = kwargs.get('id')

        if id is not None:
            return Intervention.objects.get(pk=id)

        return None

    def resolve_tasks(self, info, **kwargs):
        return Task.objects.all()

    def resolve_task(self, info, **kwargs):
        id = kwargs.get('id')
        title = kwargs.get('title')

        if id is not None:
            return Task.objects.get(pk=id)

        if title is not None:
            return Task.objects.get(title=title)

        return None

Nous avons ajouté 2 méthodes resolve_intervention et resolve_task pour respectivement récupérer une intervention et une task.‌‌ Pour récupérer une intervention spécifique, seul l'ID peut être utilisé alors que l'on peut utiliser l'ID ou le title afin de remonter une tâche.

Exemple de requête sur un objet Intervention et un objet Task

Filtrer les enregistrements

Aller chercher un résultat spécifique ou une collection complète de données est bien entendu un prérequis dans la plupart des applications développées. Mais il est nécessaire d'aller requêter des résultats plus précis notamment via les filtres. Un filtre est une condition que nous allons poser pour ne remonter que les données qui correspondent à la requête. Nous pouvons alors imaginer ne remonter que les interventions qui sont (ou ne sont pas) terminées ou les taches qui contiennent un mot spécifique.‌ Heureusement pour nous, django-filter est un module Django qui va grandement nous faciliter les choses.

pip install django-filter
installation du module django-filter

Nous pouvons désormais reprendre notre fichier schema.py et le mettre à jour afin d'y intégrer les fonctionnalités de django-filters.

from graphene import relay, ObjectType
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField

from interventions.models import Intervention, Task


class InterventionNode(DjangoObjectType):
    class Meta:
        model = Intervention
        filter_fields = ['title', 'description']
        interfaces = (relay.Node, )


class TaskNode(DjangoObjectType):
    class Meta:
        model = Task
        # Allow for some more advanced filtering here
        filter_fields = {
            'title': ['exact', 'icontains', 'istartswith'],
            'is_done': ['exact'],
        }
        interfaces = (relay.Node, )


class Query(ObjectType):
    intervention = relay.Node.Field(InterventionNode)
    all_interventions = DjangoFilterConnectionField(InterventionNode)

    task = relay.Node.Field(TaskNode)
    all_tasks = DjangoFilterConnectionField(TaskNode)

Vous l'aurez probablement noté, c'est l'apparition d'un paramètre filter_fields qui va nous permettre ces fameux filtres. Le cas le plus simple est l'implémentation sur le modèle des interventions. En effet, avec filter_fields = ['title', 'description'] nous définissons le fait de pouvoir filtrer sur les champs title et description. Avec le modèle Task, nous allons un peu plus loin en définissant comment les filtres fonctionnent.‌‌ Nous pourrons encore aller plus loin en définissant nos propres FilterSet pour préciser comment nos filters peuvent s'appliquer et sur ce point, la documentation officielle est très complète.

Ajouter de nouveaux objets avec les mutations

Jusqu’à présent, nos requêtes ont eu pour objectif de remonter la liste de nos interventions sans action possible de création ou de modification d'objets. C'est ce à quoi nous nous attachons dès à présent grâce aux mutations. ‌‌Les requêtes de mutation permettent de modifier les données. Elles sont donc utilisées pour insérer, mettre à jour ou supprimer des données. Voyons donc en détail comment implémenter ces mutations avec Graphene. Une nouvelle fois, avec Graphene-Django, nous pouvons profiter des fonctionnalités préexistantes de Django pour construire rapidement des fonctionnalités CRUD, tout en utilisant les fonctionnalités de mutation de Graphene.

from graphene import relay, ObjectType, Mutation, String, ID, Schema, Boolean, List, Int
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from graphql.language.ast import Field

from interventions.models import Intervention, Task

class TaskType(DjangoObjectType):
    class Meta:
        model = Task


class Query(ObjectType):
    interventions = List(TaskType)

    def resolve_interventions(self, info, **kwargs):
        return Task.objects.all()


class CreateTask(Mutation):
    id = Int()
    title = String()
    intervention_id = Int()

    class Arguments:
        id = Int()
        title = String()
        intervention_id = Int()


    def mutate(root, info, title, intervention_id):
        task = Task(title=title, intervention=Intervention.objects.get(pk=intervention_id))
        task.save()

        return CreateTask(
            id=task.id,
            title=task.title,
            intervention_id=task.intervention
        )


class Mutation(ObjectType):
    create_task = CreateTask.Field()
Contenu de notre fichier schema.py de notre application

Nous commençons par définir notre classe de Mutation (CreateTask) et juste après les champs qui doivent être retournés après l'exécution de la requête. La méthode mutate est la méthode qui traite les données envoyées pour créer l'enregistrement. Enfin la classe Mutation est la classe qui va mapper avec la classe CreateTask. Après avoir mis à jour notre fichier schema.py à la racine du projet, nous pouvons donc créer notre première requête de création de tâche.

import graphene

import interventions.schema


class Query(interventions.schema.Query, graphene.ObjectType):
    pass


class Mutation(interventions.schema.Mutation, graphene.ObjectType):
    pass

schema = graphene.Schema(query=Query, mutation=Mutation)
fichier schema.py à la racine de notre projet.

Conclusion

Django est un framework incroyable qui tient ses promesses et permet de développer rapidement de robustes applications. Couplé au module django-graphene, il permet aux développeurs de mettre en place un serveur GraphQL sans réelle difficulté.‌‌ Cette introduction est désormais terminée, il reste encore beaucoup à dire sur le fonctionnement des différentes briques que nous avons survolées et je ne peux que conseiller de parcourir les documentations officielles pour aller plus loin: