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 :
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 :
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 :
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 :
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 :
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 là.
>>> 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 là). 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 :
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.
- Le paradigme est différent des standards SOAP ou REST.
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
- Site officiel du langage : Introduction to GraphQL
- Une comparaison avec REST : GraphQL ou le futur des APIs
- API python : Graphene
- Discussions sur les souscriptions :
- Le code de l’exemple utilisé pour cet article :
dummyPythonGraphQLServer
1 réflexion sur “GraphQL : exemple de création d’un serveur”
Ping : GraphQL : encapsulation dans HTTP - EASYTEAM
Les commentaires sont fermés.