Intégration des Bases de Données et Couches de Résolution
Dans la conception de systèmes modernes, l'interaction entre un serveur GraphQL et une base de données relationnelle ou NoSQL est un point critique. L'utilisation d'un ORM (Object-Relational Mapping) permet d'abstraire les requêtes SQL brutes, mais son intégration dans la couche de résolution (Resolver) nécessite une attention particulière pour éviter les goulots d'étranglement.
Approche par Résolveurs Directs
La méthode la plus immédiate consiste à invoquer les méthodes de l'ORM directement depuis les résolveurs GraphQL. Cette approche sépare clairement la définition du schéma de la logique de persistance. Cependant, elle expose l'application au classique problème N+1 si les relations ne sont pas gérées avec soin.
<![CDATA[
// Implémentation avec Prisma et Apollo Server
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const typeDefs = `#graphql
type Employee {
id: ID!
fullName: String!
department: String!
hireDate: String!
}
type Query {
employees: [Employee!]!
employee(id: ID!): Employee
}
type Mutation {
addEmployee(fullName: String!, department: String!): Employee!
removeEmployee(id: ID!): Boolean!
}
`;
const resolvers = {
Query: {
employees: () => prisma.employee.findMany(),
employee: (_, args) => prisma.employee.findUnique({ where: { id: Number(args.id) } }),
},
Mutation: {
addEmployee: async (_, args) => {
return prisma.employee.create({ data: args });
},
removeEmployee: async (_, args) => {
await prisma.employee.delete({ where: { id: Number(args.id) } });
return true;
}
}
};
const server = new ApolloServer({ typeDefs, resolvers });
startStandaloneServer(server).then(({ url }) => console.log(`Serveur démarré sur ${url}`));
]]>
Optimisation via le Motif DataLoader
Pour pallier les requêtes N+1, l'introduction de DataLoader est indispensable. Ce motif permet de regrouper (batch) et de mettre en cache les requêtes de base de données au sein d'un même tick de la boucle d'événements, réduisant drastiquement la charge sur le SGBD.
<![CDATA[
import DataLoader from 'dataloader';
import CategoryModel from './models/Category';
import ItemModel from './models/Item';
const buildCategoryLoader = () => new DataLoader(async (categoryIds) => {
const categories = await CategoryModel.find({ _id: { $in: categoryIds } });
const categoryMap = new Map(categories.map(cat => [cat._id.toString(), cat]));
return categoryIds.map(id => categoryMap.get(id.toString()) || null);
});
const buildItemsByCategoryLoader = () => new DataLoader(async (categoryIds) => {
const items = await ItemModel.find({ categoryId: { $in: categoryIds } });
const groupedItems = new Map();
items.forEach(item => {
const catId = item.categoryId.toString();
if (!groupedItems.has(catId)) groupedItems.set(catId, []);
groupedItems.get(catId).push(item);
});
return categoryIds.map(id => groupedItems.get(id.toString()) || []);
});
const resolvers = {
Category: {
items: (parent, _, context) => context.itemsLoader.load(parent._id)
},
Query: {
category: (_, { id }, context) => context.categoryLoader.load(id)
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
categoryLoader: buildCategoryLoader(),
itemsLoader: buildItemsByCategoryLoader()
})
});
]]>
Paradigmes de Développement : Schema-First et Déclaratif
Génération de Code Schema-First
L'approche Schema-First impose de définir le contrat d'API avant l'implémentation. Des outils comme GraphQL Code Generator permettent de dériver automatiquement les types TypeScript et les modèles ORM, garantissant une cohérence stricte entre le schéma et le code.
<![CDATA[
# schema.graphql
type Author {
id: ID!
firstName: String!
lastName: String!
books: [Book!]!
}
type Book {
id: ID!
title: String!
publishedYear: Int!
author: Author!
}
type Query {
authors: [Author!]!
books: [Book!]!
}
]]>
<![CDATA[
# codegen.yml
schema: './schema.graphql'
generates:
./src/types/graphql-types.ts:
plugins:
- 'typescript'
- 'typescript-resolvers'
./src/models/prisma-models.ts:
plugins:
- 'prisma-nexus-generator'
]]>
CRUD Déclaratif par Décorateurs
Pour les équipes cherchant à minimiser le code boilerplate, l'utilisation de décorateurs (comme avec type-graphql) permet de générer à la fois le schéma GraphQL et les entités ORM à partir d'une seule définition de classe.
<![CDATA[
import { ObjectType, Field, ID, InputType, Resolver, Query, Mutation, Arg } from 'type-graphql';
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, getRepository } from 'typeorm';
@ObjectType()
@Entity()
export class Project {
@Field(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column()
title: string;
@Field()
@Column({ default: false })
isActive: boolean;
@Field()
@CreateDateColumn()
launchedAt: Date;
}
@InputType()
export class ProjectCreationInput {
@Field()
title: string;
}
@Resolver(Project)
export class ProjectResolver {
private repo = getRepository(Project);
@Query(() => [Project])
async fetchProjects() {
return this.repo.find();
}
@Mutation(() => Project)
async initiateProject(@Arg('input') input: ProjectCreationInput) {
const newProject = this.repo.create({ title: input.title });
return this.repo.save(newProject);
}
}
]]>
Architecture Fédérée et Microservices
À mesure que les systèmes évoluent, un monolithe GraphQL devient difficile à maintenir. GraphQL Federation permet de décomposer le schéma en plusieurs sous-graphes (Subgraphs) indépendants, orchestrés par un routeur (Supergraph).
Concepts Fondamentaux de la Fédération
| Concept | Description | Cas d'Usage |
|---|---|---|
| Supergraph | Le schéma unifié exposé aux clients. | Point d'entrée unique pour les applications front-end. |
| Subgraph | Un service GraphQL autonome gérant un domaine spécifique. | Service "Paiement" ou service "Catalogue". |
| Entity | Un objet métier partagé et référencé entre plusieurs sous-graphes. | L'entité User enrichie par les services "Profil" et "Commandes". |
Système de Directives
La Fédération s'appuie sur des directives pour mapper les relations entre les sous-graphes.
<![CDATA[
# Service Catalogue
type Product @key(fields: "sku") {
sku: ID!
name: String!
price: Float!
}
# Service Stock
type Product @key(fields: "sku") {
sku: ID!
stockLevel: Int!
warehouseLocation: String! @external
}
type Shipment {
id: ID!
productSku: ID! @external
destination: String! @requires(fields: "productSku")
}
]]>
Gestion des Performances et Résilience
Dans un environnement distribué, le routeur doit optimiser le plan d'exécution des requêtes. L'implémentation de stratégies de cache à plusieurs niveaux (client, routeur, sous-graphe) est cruciale. De plus, l'utilisation de pattern comme le Circuit Breaker protège le système contre les cascades de pannes.
<![CDATA[
import { CircuitBreaker } from 'opossum';
const subgraphCircuit = new CircuitBreaker(fetchSubgraphData, {
errorThresholdPercentage: 50,
resetTimeout: 60000,
timeout: 3000
});
subgraphCircuit.on('open', () => console.warn('Circuit ouvert pour le sous-graphe'));
subgraphCircuit.fallback(() => getFallbackData());
]]>
Communications en Temps Réel et Abonnements
GraphQL ne se limite pas aux requêtes et mutations. Le mécanisme de subscriptions permet de maintenir une connexino persistante via WebSocket, offrant une alternative élégante au polling HTTP pour les mises à jour en temps réel.
Implémentation du Protocole graphql-ws
Le standard graphql-ws est aujourd'hui privilégié pour gérer les abonnements. Côté client, la souscription agit comme un flux de données continu.
<![CDATA[
import { createClient } from 'graphql-ws';
const wsClient = createClient({
url: 'wss://api.example.com/graphql',
});
const unsubscribe = wsClient.subscribe(
{
query: `
subscription OnStockChange($productId: ID!) {
stockUpdated(productId: $productId) {
sku
newQuantity
updatedAt
}
}
`,
variables: { productId: 'prod-123' }
},
{
next: (data) => console.log('Mise à jour reçue:', data),
error: (err) => console.error('Erreur de subscription:', err),
complete: () => console.log('Subscription terminée')
}
);
]]>