GraphQL : exemple de création d’un serveur

Présentation

GraphQL est un langage d’API récent (publié en 2015) créé par Facebook. Il se présente comme un concurrent direct à REST avec un paradigme différent.
L’objectif de cet article n’est pas de présenter les concepts et la théorie GraphQL, le site officiel le fait déjà.
Il s’agit plutôt de proposer un exemple d’implémentation de serveur en Python (Oracle n’ayant pas encore implémenté ce langage dans la suite « Fusion MiddleWare »). Cet exemple pouvant ensuite être repris et adapté pour de potentiels futurs projets utilisant cette technologie.

Implémentation et tests

Mise en situation

La situation proposée en exemple est celle d’un magasin devant gérer des clients, leurs commandes et les articles proposés :

  • Un client est une personne avec un nom/prénom et une adresse.
  • Le stock est une simple liste d’articles avec leurs prix.
  • Une commande est passée à un client à une certaine date et une liste d’articles est agrémentée de la quantité demandée.

Modèle de données

La base de données sera modélisée en SQL de cette façon :

diagramme entités-relations
diagramme entités-relations

Le stock est représenté par la table ITEM et une commande par le couple PURCHASE_ORDER / ORDER_LINE.
Le code SQL pour une base SQLite étant assez trivial :

-- clean DB
DROP TABLE IF EXISTS ORDER_LINE;
...
-- create DB
CREATE TABLE ITEM (
	ID INTEGER PRIMARY KEY AUTOINCREMENT,
	NAME VARCHAR UNIQUE NOT NULL,
	PRICE FLOAT NOT NULL CHECK(PRICE > 0)
);
...

Pour chaque objet métier, un objet logique est créé. La structure sera utilisée est :

modélisation des entités
modélisation des entités

Le code étant lui aussi simple :

''' INFO :
in order to avoid circular references, there is absolutly no link between entities
i.e : a client does not reference an address (& an address does not reference clients)
'''
class Entity():
    def __init__(self,id):
        self.id = id
    def __eq__(self,other):
        return self.id == other.id
    def __hash__(self):
        return hash(self.id)
class Item(Entity):
    def __init__(self,name,price,id=None):
        super().__init__(id)
        self.name = name
        self.price = price
    def __eq__(self,other):
        return super().__eq__(other) \
            and self.name == other.name \
            and self.price == other.price
    def __hash__(self):
        return hash(((super().__hash__(), self.name, self.price)))
...

Il est déjà possible de noter que bien que la base de données contienne des liens forts (clés étrangères) entre les objets, ce n’est pas le cas sur les entités logiques associées. Par exemple, l’association client / adresse disparaît.
Cela évite d’avoir des problèmes de dépendance circulaire lors de la résolution des requêtes de l’API. En effet, chaque requête GraphQL résoudra les dépendances elle-même selon le contexte.
Par exemple :

  • Si on demande une adresse, l’ensemble des clients associés sera construit dynamiquement via un mécanisme spécifique.
  • Réciproquement, si on demande un client, son adresse sera aussi résolue mais dans un autre mécanisme.

Accès et manipulation des données

Les 4 méthodes CRUD standards seront utilisées pour respectivement créer, lire, mettre-à-jour et supprimer les données :

accès aux données
accès aux données

Ce qui donne le code suivant, par exemple pour lire des adresses :

# data access object
class AddressDAO():
    @staticmethod
    def read(ids=None, houseNumber=None, street=None, zipCode=None, city=None, clientsIds=None, limit=None, offset=None):
        statement = "SELECT A.HOUSE_NUMBER, A.STREET, A.ZIPCODE, A.CITY, A.ID FROM ADDRESS A"
        parameters = list()
        # joins
        if clientsIds:
            statement += " JOIN CLIENT C ON C.ADDRESS_ID = A.ID"
        # where
        statement += " WHERE 0=0"
        if ids:
            statement += " AND A.ID IN ("+','.join(['?']*len(ids))+")"
            parameters += ids
        if houseNumber:...
        if street:...
        if zipCode:...
        if city:...
        if clientsIds:...
        # order
        statement += " ORDER BY A.ID ASC"
        # shift
        if limit:
            statement += " LIMIT ?"
            parameters.append(limit)
        if offset:...
        connection = connect(DB)
        cursor = connection.cursor()
        raisedExeption = None
        addresses = list()
        try:
            results = cursor.execute(statement,parameters)
            for result in results:
                address = Address(*result)
                addresses.append(address)
        except Exception as exeception:
            raisedExeption = exeception
        finally:
            cursor.close()
            connection.close()
            if raisedExeption : raise raisedExeption
        return addresses

Les points à noter sont :

  • La manipulation ensembliste des données pour limiter le nombre d’appels à la base.
  • Dans notre exemple, le paramètre « clientsIds » permet de récupérer les adresses associées à des clients.
    Cette solution est utile pour résoudre les liens entre les données (voir les remarques du paragraphe précédent).
    Des mécanismes similaires sont utilisés dans les autres objets.

Contrats d’interface

Entrées

Pour les entrées, nous avons des paramètres communs classiques pour toutes les requêtes :

  • Mise-à-jour et suppression : ID
    Obligatoire pour une mise-à-jour, pas pour une suppression.
  • Lecture : ID, limite et point de départ.
    Les 2 derniers étant une bonne pratique pour gérer un affichage paginé.
    Aucun de ces paramètres n’est obligatoire.

Note : Il n’y a pas de paramètre commun pour la création. L’ID sera créé et retourné une fois l’opération terminée.
Ensuite, pour chaque objet métiers, des paramètres spécifiques pour chaque objet :

  • Adresse : N° de rue, rue, ville et CP, pour toutes les requêtes
  • Client : nom et prénom, pour toutes les requêtes.
  • Article : nom et prix, pour toutes les requêtes.
  • Commande :
    • Lecture et la suppression : dates de création minimum et maximum.
    • Création et mise-à-jour : date de création exacte.

Pour lecture et la suppression, les paramètres sont optionnels. Pour la création et mise-à-jour, ils sont obligatoires.
Ainsi, les diagrammes UML correspondants sont :

UML entrée création et MàJ
UML entrée création et MàJ
UML entrée lecture et suppression
UML entrée lecture et suppression

Les codes appliqués (générique et spécifique) pour les entrées de l’objet client :

class DeleteInput():
    ids = List(ID)
class ReadInput(DeleteInput):
    limit = Int()
    offset = Int()
class UpdateInput():
    id = ID(required=True)
#######################################################
class DeleteClientsInput(DeleteInput, InputObjectType):
    firstName = String()
    lastName = String()
class ReadClientsInput(ReadInput, DeleteClientsInput):
    pass
class CreateClientsInput(InputObjectType):
    firstName = String(required=True)
    lastName = String(required=True)
    addressId = ID(required=True)
class UpdateClientsInput(UpdateInput, CreateClientsInput):
    pass

Pour finir, on peut noter que GraphQL ne connait que 5 types primitifs (entier, décimal, chaîne de caractères booléen et ID) mais que l’on peut les encapsuler dans un objet dédié aux entrées (ici InputObjectType).

Sorties

Sûrement l’une des parties les plus compliquées de l’implémentation GraphQL. En effet, et comme dit plus haut, il faut éviter les références circulaires.
Par exemple, si on part d’une adresse, alors on peut remonter aux clients. Et en partant d’un client, on peut aussi trouver son adresse. Mais les clients / adresses doivent être des objets bien séparés.
Du coup, il faut tracer les graphes pour chaque cas, ce qui est complexe mais nécessaire :

UML sortie
UML sortie

Le code associé (générique et spécifique) est lui aussi complexe :

class Query(ObjectType):
    readClients = Field(List(NonNull(RootClientField)),required=True, readClientsInput=ReadClientsInput(required=True), withAddress=Boolean(), withPurchaseOrders=Boolean(), withOrderLines=Boolean())
    readAddresses = Field(List(NonNull(RootAddressField)),required=True, readAddressesInput=ReadAddressesInput(required=True), withClients=Boolean(), withPurchaseOrders=Boolean(), withOrderLines=Boolean())
    readPurchaseOrders = Field(List(NonNull(RootPurchaseOrderField)),required=True, readPurchaseOrdersInput=ReadPurchaseOrdersInput(required=True), withOrderLines=Boolean(), withClient=Boolean(), withAddress=Boolean())
    readItems = Field(List(NonNull(RootItemField)),required=True, readItemsInput=ReadItemsInput(required=True), withClients=Boolean(), withAddress=Boolean(), withPurchaseOrders=Boolean())
    '''INFO : keep all parameters, even the ones you do not use later
    they will be used by graphen framework
    i.e : withAddress, withClient(s), withPurchaseOrders, withOrderLines'''
    def resolve_readClients(self, _, readClientsInput, withAddress=True, withPurchaseOrders=True, withOrderLines=True):
        leafClientFields = LeafClientField.resolveClients(ids=readClientsInput.ids, firstName=readClientsInput.firstName, lastName=readClientsInput.lastName, limit=readClientsInput.limit, offset=readClientsInput.offset)
        rootClientFields = list()
        for leafClientField in leafClientFields:
            rootClientField = RootItemField.ClientItemField(id=leafClientField.id, firstName=leafClientField.firstName, lastName=leafClientField.lastName)
            rootClientFields.append(rootClientField)
        return rootClientFields
...
##########################################################################################################################################################################################################################
class LeafAddressField(CommonField):
    houseNumber = String(required=True)
    street = String(required=True)
    zipCode = String(required=True)
    city = String(required=True)
    def resolve_houseNumber(self,_):
        return self.houseNumber
    def resolve_street(self,_):
        return self.street
    def resolve_zipCode(self,_):
        return self.zipCode
    def resolve_city(self,_):
        return self.city
    # shared resolver
    @staticmethod
    def resolveAddresses(ids=None, houseNumber=None, street=None, zipCode=None, city=None, clientsIds=None, limit=None, offset=None):
        # read data
        readAddresses = AddressDAO.read(ids, houseNumber, street, zipCode, city, clientsIds, limit, offset)
        # graphQL response
        addressesField = list()
        for address in readAddresses:
            addressField = LeafAddressField(id=address.id, houseNumber=address.houseNumber, street=address.street, zipCode=address.zipCode, city=address.city)
            addressesField.append(addressField)
        return addressesField

Pour décrypter cela, il faut comprendre les différents objets :

  • Query : regroupe les actions de lecture.
    Les 4 attributs (client, adresse, commande et article) sont les 4 objets que l’on peut lire.
  • LeafAddressFields : représente les attributs propres à l’adresse : n° de rue, rue, ville et CP.
    L’attribut adresse utilisé dans l’objet Query y fait référence.
    On a un objet similaire pour le client, la commande et l’article.

Pour simplifier la compréhension, les objets ont été divisés en 3 catégories classiques pour des arbres :

  • Racine : les objets dont on part à la base de la requête.
    Ceux sont eux qui vont regrouper les objets secondaires.
    Ils sont reconnaissables dans le code par le préfixe « root ».
  • Nœuds : les objets intermédiaires, qui peuvent eux-même encapsuler d’autres objets (mais jamais des racines !).
    Ils sont reconnaissables dans le code par le préfixe « node ».
  • Feuilles : les objets les plus basiques : ils ne font référence à aucun autre objet et ne contiennent que les attributs nécessaires.
    Ceux sont eux qui font appel à la base de données.
    Enfin, les racines et nœuds héritent de ces feuilles.
    Ils sont reconnaissables dans le code par le préfixe « leaf ».

Note : les méthodes « resolve » seront vues aux point suivant (Lecture).

Lecture

Résolveurs

Afin de pouvoir lire les données, il ne manque plus que les méthodes pour renvoyer les objets de sorties en fonction des entrées : ce sont les méthodes « resolve » vues dans l’extrait de code du paragraphe précédent (Sorties).
Les résolveurs placés dans les objets racines prennent en entrées les objets définis plus haut (§ « Entrées ») et retournent les sorties attendues. Ces résolveurs peuvent en appeler d’autres autant que nécessaire pour les objets complexes.
Pour les objets intermédiaires (nœuds et feuilles), on crée les résolveurs dont on a besoin, avec les E/S correspondantes. On est un peu plus libre sur les signatures, tant que l’on utilise des types connus de GraphQL. Là aussi, l’appel à des sous-résolveurs est possible (attention aux boucles).

Test des requêtes

Et maintenant, nous voilà libre de saisir des requêtes de lecture et de voir le résultat.
Note : Une invite de commande basique se trouve .

>>> request
query ($readAddressesInput:ReadAddressesInput!,$withClients:Boolean!,$withPurchaseOrders:Boolean!,$withOrderLines:Boolean!) {
    readAddresses (readAddressesInput:$readAddressesInput,withClients:$withClients,withPurchaseOrders:$withPurchaseOrders,withOrderLines:$withOrderLines) {
        houseNumber,
        street,
        zipCode,
        city,
        clients @include(if: $withClients) {
            firstName,
            lastName,
            purchaseOrders @include(if: $withPurchaseOrders) {
                id,
                creationDate,
                orderLines @include(if: $withOrderLines) {
                    item {
                        name,
                        price
                    },
                    quantity
                }
            }
        }
    }
}
>>> parameters
{
	"readAddressesInput": {
		"street": "st.",
		"city": "le"
	},
	"withClients": true,
	"withPurchaseOrders": true,
	"withOrderLines": false
}
<<< data
{
    "readAddresses": [
        {
            "houseNumber": "765",
            "street": "Thomas St.",
            "zipCode": "7024",
            "city": "Fort Lee",
            "clients": [
                {
                    "firstName": "Stephen",
                    "lastName": "King",
                    "purchaseOrders": [
                        {
                            "id": "16",
                            "creationDate": 20080828
                        }
                    ]
                },
                {
                    "firstName": "Stephen",
                    "lastName": "Hawking",
                    "purchaseOrders": [
                        {
                            "id": "6",
                            "creationDate": 20110306
                        }
                    ]
                }
            ]
        },
        {
            "houseNumber": "23",
            "street": "Ridgeview St.",
            "zipCode": "80123",
            "city": "Littleton",
            "clients": [
                {
                    "firstName": "Taylor",
                    "lastName": "Swift",
                    "purchaseOrders": []
                }
            ]
        },
        ...
    ]
}
>>> request
query ($readPurchaseOrdersInput:ReadPurchaseOrdersInput!,$withOrderLines:Boolean!,$withClient:Boolean!,$withAddress:Boolean!) {
    readPurchaseOrders (readPurchaseOrdersInput:$readPurchaseOrdersInput,withOrderLines:$withOrderLines,withClient:$withClient,withAddress:$withAddress) {
        id,
        creationDate,
        orderLines @include(if: $withOrderLines) {
            item {
                name,
                price
            },
            quantity
        },
        client @include(if: $withClient) {
            firstName,
            lastName,
            address @include(if: $withAddress) {
                houseNumber,
                street,
                zipCode,
                city
            }
        }
    }
}
>>> parameters
{
	"readPurchaseOrdersInput": {
		"minimumCreationDate":20120622,
		"maximumCreationDate":20150125
	},
	"withOrderLines": true,
	"withClient": true,
	"withAddress": true
}
<<< data
{
    "readPurchaseOrders": [
        {
            "id": "7",
            "creationDate": 20121227,
            "orderLines": [
                {
                    "item": {
                        "name": "wheat",
                        "price": 2.1
                    },
                    "quantity": 64
                },
                ...
            ],
            "client": {
                "firstName": "Frank",
                "lastName": "Zappa",
                "address": {
                    "houseNumber": "3",
                    "street": "Jockey Hollow Dr.",
                    "zipCode": "6606",
                    "city": "Bridgeport"
                }
            }
        },
        {
            "id": "10",
            "creationDate": 20120622,
            "orderLines": [
                {
                    "item": {
                        "name": "melon (block)",
                        "price": 25.15
                    },
                    "quantity": 53
                },
                ...
            ],
            "client": {
                "firstName": "Anne",
                "lastName": "Hathaway",
                "address": {
                    "houseNumber": "3",
                    "street": "Jockey Hollow Dr.",
                    "zipCode": "6606",
                    "city": "Bridgeport"
                }
            }
        },
        ...
    ]
}

Manipulation

Création des objets

En GraphQL, une manipulation (création, mise-à-jour ou suppression) se nomme aussi « mutation ».
Ici, pour chaque mutation de chaque entité, on crée un objet dédié avec :

  • La classe interne Arguments : les paramètres d’entrées, dans la même logique que ceux utilisés pour la lecture.
  • La méthode mutate : action à réaliser, spécifique à la manipulation voulue.
    Là aussi, la logique ressemble à celle des résolveurs utilisés en lecture.
    Comme il faut obligatoirement renvoyer quelque-chose, ici :

    • Création : renvoie le même type d’objet qu’une lecture et l’ID sera automatiquement incrémenté puis renvoyé.
    • Mise-à-jour et suppression : renvoie un simple booléen (vrai pour succès, faux pour échec).

Par exemple, le code associé pour créer un client :

class CreateClients(Mutation):
    class Arguments:
        createClientsInput = List(NonNull(CreateClientsInput), required=True)
        withAddress = Boolean()
    readClients = Field(List(NonNull(NodeClientField)), required=True)
    '''INFO : keep all parameters, even the ones you do not use later
    they will be used by graphen framework
    i.e : withAddress, withClient(s), withPurchaseOrders, withOrderLines'''
    def mutate(self, _, createClientsInput, withAddress=True):
        # create each client (and associated address id)
        entitiesClientsInputs = dict()
        for lineClientsInput in createClientsInput:
            entityClient = Client(firstName=lineClientsInput.firstName, lastName=lineClientsInput.lastName)
            entitiesClientsInputs[entityClient] = lineClientsInput.addressId
        clientOutputs = ClientDAO.create(entitiesClientsInputs)
        return CreateClients(readClients=clientOutputs)

Test des requêtes

Et maintenant, nous voilà libre de saisir des requêtes de manipulation des données.

>>> request
mutation {
    createItems (createItemsInput:[
    {
        name:"vvrench", price:"1.0"
    },{
        name:"corkscrew", price:"7.8"}
    ]) {
        readItems {
            id,
            name,
            price
        }
    }
}
<<< data
{
    "createItems": {
        "readItems": [
            {
                "id": "382",
                "name": "vvrench",
                "price": 1.0
            },
            {
                "id": "383",
                "name": "corkscrew",
                "price": 7.8
            }
        ]
    }
}
>>> request
mutation {
    updateItems (updateItemsInput:[
    {
        id: 382, name:"wrench", price:"1.0"
    },{
        id: 383, name:"corkscrew", price:"7.85"}
    ]) {confirmation}
}
<<< data
{
    "updateItems": {
        "confirmation": true
    }
}
>>> request
{
    readItems (readItemsInput:{ids:[382,383]}) {
        name,
        price
    }
}
<<< data
{
    "readItems": [
        {
            "name": "wrench",
            "price": 1.0
        },
        {
            "name": "corkscrew",
            "price": 7.85
        }
    ]
}
>>> request
mutation {
    deleteItems (deleteItemsInput:{ids:[382,383]}) {confirmation}
}
<<< data
{
    "deleteItems": {
        "confirmation": true
    }
}
>>> request
{
    readItems (readItemsInput:{ids:[382,383]}) {id}
}

>>> parameters

<<< data
{
    "readItems": []
}

Souscription

Création des objets

Une des grandes évolutions de GraphQL est la possibilité de souscrire à des événements, et donc d’une certaine manière de reproduire des mécanismes existants sur des systèmes de messages (par exemple JMS).
Toutefois, cette fonctionnalité est complexe à mettre en œuvre et le manque de documentation et d’exemples n’aide pas. La solution proposée dans cet article est donc à prendre avec beaucoup de recul, voire à refaire si quelque chose de meilleur existe.
Dans notre cas (utilisation du framework Python Graphène), une souscription oblige à renvoyer un objet de type Observable. Cela dans le but de gérer le flux de données dans un design-pattern Observer (pour rappel, ici et ). Le problème, c’est que le framework surcharge automatiquement l’objet observable par un observable « anonyme » en effaçant les attributs spécifiques de l’observable d’origine :(. Du coup, l’observateur mis en frontal n’étant pas du tout géré par le framework, c’est lui qui porte toute la complexité. L’observable se résume alors à son implémentation minimale.
Pour l’observateur justement, on part du principe qu’une souscription est à la base une opération de lecture. On lui rajoute juste une fréquence (par exemple, 1 fois par seconde) et un nombre de répétition maximum (par exemple, 20). Ainsi, à l’initialisation de l’observateur, on extrait d’un côté les paramètres propres à la lecture et de l’autre côté ceux pour la gestion du temps.
Le diagramme UML de cette implémentation est donc :

UML souscription
UML souscription

Et son code :

class SubscriptionObserver(Observer):
    # inheritance
    def on_next(self, value):
        result = schema.execute(self.query, variables=self.variables)
        return result
    def on_completed(self, value):
        return
    def on_error(self, value):
        return
    # constructor
    def __init__(self, subscription,variables):
        # INFO : a subscription is just a query repeated in time
        self.query = subscription.replace("subscription", "query")
        # INFO : remove useless parameters for query (dirty but efficient)
        self.query = sub(",(\s?)+\$period(\s?)+:(\s?)+Int!",'',self.query) # $period:Int!
        self.query = sub(",(\s?)+\$occurencesNumber(\s?)+:(\s?)+Int!",'',self.query) # $occurencesNumber:Int!
        self.query = sub(",(\s?)+period(\s?)+:(\s?)+\$period", '', self.query) # period:$period
        self.query = sub(",(\s?)+occurencesNumber(\s?)+:(\s?)+\$occurencesNumber", '', self.query) # occurencesNumber:$occurencesNumber
        self.period = variables.get("period")
        self.occurencesNumber = variables.get("occurencesNumber")
        # INFO : do a copy to separate subscription & query parameters
        self.variables = dict(variables)
        del self.variables["period"]
        del self.variables["occurencesNumber"]
class Subscription(ObjectType):
    # INFO : input parameters are almost the same than queries one, except we add period (in second) and occurences number
    readClients = Field(List(NonNull(RootClientField)),required=True, readClientsInput=ReadClientsInput(required=True), period=Int(required=True), occurencesNumber=Int(required=True), withAddress=Boolean(), withPurchaseOrders=Boolean(), withOrderLines=Boolean())
    readAddresses = Field(List(NonNull(RootAddressField)),required=True, readAddressesInput=ReadAddressesInput(required=True), period=Int(required=True), occurencesNumber=Int(required=True), withClients=Boolean(), withPurchaseOrders=Boolean(), withOrderLines=Boolean())
    readPurchaseOrders = Field(List(NonNull(RootPurchaseOrderField)),required=True, readPurchaseOrdersInput=ReadPurchaseOrdersInput(required=True), period=Int(required=True), occurencesNumber=Int(required=True), withOrderLines=Boolean(), withClient=Boolean(), withAddress=Boolean())
    readItems = Field(List(NonNull(RootItemField)),required=True, readItemsInput=ReadItemsInput(required=True), period=Int(required=True), occurencesNumber=Int(required=True), withClients=Boolean(), withAddress=Boolean(), withPurchaseOrders=Boolean())
    '''
    WARN : There is many reason we return a default observABLE object,
    all due to the fact RxPy framework overload our observable object to an anonymous one:
     - custom attributs are lost
     - the subcribe method is embedded in others one, witch are difficult to unpile
    Therefore, all algorithm will be implmented in observER object
    '''
    def resolve_readClients(self, _, readClientsInput, period, occurencesNumber, withAddress=True, withPurchaseOrders=True, withOrderLines=True):
        return BlockingObservable()
    def resolve_readAddresses(self, _, readAddressesInput, period, occurencesNumber, withClients=True, withPurchaseOrders=True, withOrderLines=True):
        return BlockingObservable()
    def resolve_readPurchaseOrders(self, _, readPurchaseOrdersInput, period, occurencesNumber, withOrderLines=True, withClient=True, withAddress=True):
        return BlockingObservable()
    def resolve_readItems(self, _, readItemsInput, period, occurencesNumber, withClients=True, withAddress=True, withPurchaseOrders=True):
        return BlockingObservable()

Note : L’observateur sera ensuite lancé depuis un processus indépendant (Thread) pour rester parfaitement asynchrone.

Test des requêtes

Dans cette implémentation, les réponses aux souscriptions sont écrites dans un fichier. Ce fichier sera rempli au fil de l’eau et l’interface de saisie sera toujours disponible pour traiter d’autres requêtes.

# 1st moment : subscription start but no data match
subscription ($readItemsInput:ReadItemsInput!,$period:Int!,$occurencesNumber:Int!) {
readItems (readItemsInput:$readItemsInput,period:$period,occurencesNumber:$occurencesNumber) {
    name,
    price,
    }
}
{
    "readItemsInput": {"name":"screw"},
    "period":1,
    "occurencesNumber":20
}
# 2nd moment : new data creation
mutation {
    createItems (createItemsInput:[
    {
        name:"screwdriver", price:"1.0"
    },{
        name:"corkscrew", price:"7.8"}
    ]) {
        readItems {id}
    }
}
# 3rd & last moment : data are erased
mutation {
    deleteItems (deleteItemsInput:{name:"screw"}) {confirmation}
}
# 1st moment : there is no data yet
<<< data
{
    "readItems": []
}
# 2nd moment : the new data appears
<<< data
{
    "readItems": [
        {
            "name": "screwdriver",
            "price": 1.0
        },
        {
            "name": "corkscrew",
            "price": 7.8
        }
    ]
}
# 3rd & last moment : data disappears
<<< data
{
    "readItems": []
}

Gestion des erreurs

Le framework remplit lui-même les erreurs lorsqu’une exception est levée.

>>> request
mutation {
    createItems (createItemsInput:[
    {
        name:"screwdriver", price:"1.0"
    ]) {
        readItems {id}
    }
}
<<< data
{
    "createItems": null
}
<<< error #0
UNIQUE constraint failed: ITEM.NAME
>>> request
{
    readItems (readItemsInput:{name:0}) {id}
}
<<< data
null
<<< error #0
Argument "readItemsInput" has invalid value {name: 0}.
In field "name": Expected type "String", found 0.

Introspection

Il est possible d’utiliser automatiquement le mécanisme instropection pour retrouver les signatures de nos méthodes.

>>> request
{
    CreateAddresses : __type(name: "CreateAddresses") {...instrospection}
    RootAddressField : __type(name: "RootAddressField") {...instrospection}
    LeafAddressField : __type(name: "LeafAddressField") {...instrospection}
}
fragment instrospection on __Type {
    name
    kind,
    fields {
        name,
        type {
            kind,
            ofType {kind}
        }
    }
}
<<< data
{
    "CreateAddresses": {
        "name": "CreateAddresses",
        "kind": "OBJECT",
        "fields": [
            {
                "name": "readAddresses",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "LIST"
                    }
                }
            }
        ]
    },
    "RootAddressField": {
        "name": "RootAddressField",
        "kind": "OBJECT",
        "fields": [
            {
                "name": "id",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "SCALAR"
                    }
                }
            },
            {
                "name": "houseNumber",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "SCALAR"
                    }
                }
            },
            {
                "name": "street",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "SCALAR"
                    }
                }
            },
            {
                "name": "zipCode",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "SCALAR"
                    }
                }
            },
            {
                "name": "city",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "SCALAR"
                    }
                }
            },
            {
                "name": "clients",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "LIST"
                    }
                }
            }
        ]
    },
    "LeafAddressField": {
        "name": "LeafAddressField",
        "kind": "OBJECT",
        "fields": [
            {
                "name": "id",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "SCALAR"
                    }
                }
            },
            {
                "name": "houseNumber",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "SCALAR"
                    }
                }
            },
            {
                "name": "street",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "SCALAR"
                    }
                }
            },
            {
                "name": "zipCode",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "SCALAR"
                    }
                }
            },
            {
                "name": "city",
                "type": {
                    "kind": "NON_NULL",
                    "ofType": {
                        "kind": "SCALAR"
                    }
                }
            }
        ]
    }
}

Bilan

A travers cette mise en situation, il se révèle que le langage d’API GraphQL :

  • Possède de nombreux points positifs :
    • Utilisation d’un typage fort qui assure la cohérence des données.
    • Une description claire et dynamique des champs attendus en retour.
      Le client recevra donc exactement ce qu’il attend.
    • Possibilité de travailler sur des lots entiers de données et passer plusieurs traitements dans une seule requête.
      Contrairement à REST, qui fonctionne sur des ressources spécifiques et unitaires, les appels sont donc réduits.
    • Le serveur se base sur une représentation logique des données et de leurs liens.
      Les clients sont libres de formuler leurs requêtes tant que le modèle de données est respecté.
      L’introspection permet au client d’inférer cette logique, ce qui rend obsolètes les formats statiques de description (WADL et WSDL).
    • L’exposition et le transport des données sont libres.
      Là où REST repose uniquement sur HTTP, GraphQL n’a pas de limite théorique : HTTP et JMS pour les classiques de la SOA, mais Kafka ou Sqoop sont possibles les plus modernes ou X.25 pour le côté vintage.
    • Le mécanisme de souscription offre une possibilité de lecture asynchrone, comparable à ce que l’on peut faire avec JMS.
  • Mais aussi quelques inconvénients, surtout dus à sa jeunesse :
    • Le paradigme est différent des standards SOAP ou REST.
      Autant cela est intellectuellement stimulant, autant cela peut-être un frein pour une adoption fasse aux concurrents : il semble toujours plus sécurisant de se baser sur une solution existante classique, universelle et ayant fait ses preuves.
    • Il existe encore peu de projets utilisant cette technologie, comparé à REST ou SOAP.
      Il faut donc parfois pallier au manque d’exemples et d’expériences par des essais/erreurs qui prennent du temps et font planer une incertitude sur le produit final.
    • Les frameworks n’implémentent ou ne commente pas complètement toutes les spécifications du langage.
      Il faut parfois inventer soit même les solutions.

Pour le moment notre serveur n’expose pas ses services à l’extérieur, ce qui est rédhibitoire pour une API. Un prochain article sera donc axé sur une encapsulation dans HTTP.

Références

1 réflexion sur “GraphQL : exemple de création d’un serveur”

  1. Ping : GraphQL : encapsulation dans HTTP - EASYTEAM

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *