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.
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.
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.
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.
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:
- Document officielle de GraphQL : https://graphql.org/learn/
- Graphene : https://docs.graphene-python.org/en/latest/quickstart/