22"
Optimisation d'une API GraphQL avec du cache serveur

27 Août 2021
Clément Rivaille
Est-ce que j'en ai vraiment besoin ?

Cache HTTP
type Post @cacheControl(maxAge: 120) {
id: ID!
title: String!
author: Author!
votes: Int
comments: [Comment!] @cacheControl(maxAge: 60)
readByCurrentUser: Boolean! @cacheControl(maxAge: 10, scope: PRIVATE)
}- Chaque type a par défaut un maxAge 0, scope PUBLIC
- Les types primitifs héritent du parent
- Le maxAge de la requête est le plus petit présent dans l'arbre
- Le cache est PRIVATE si un seul nœud l'est
import { BaseRedisCache } from 'apollo-server-cache-redis';
import responseCachePlugin from 'apollo-server-plugin-response-cache';
import Redis from 'ioredis';
const cache = new BaseRedisCache({
client: new Redis({ ...config.redis })
});
const server = new ApolloServer({
// ...
plugins: [
responseCachePlugin({
cache,
sessionId: request =>
(request.context as GraphQLAPIContext).uid || null,
shouldReadFromCache: async ({ request, context }) => {
if (request.http && request.http.headers.get('Cache-Control') === 'no-cache') {
return false;
}
const apiContext = context as GraphQLAPIContext;
if (await context.isUserAdmin()) {
return false;
}
return true;
},
shouldWriteToCache: async ({ request, context }) => {
// [...] same
}
}) as ApolloServerPlugin
],
// tracing: true,
// cacheControl: true
})import { BaseRedisCache } from 'apollo-server-cache-redis';
import responseCachePlugin from 'apollo-server-plugin-response-cache';
import Redis from 'ioredis';
const cache = new BaseRedisCache({
client: new Redis({ ...config.redis })
});
const server = new ApolloServer({
// ...
plugins: [
responseCachePlugin({
cache
})Dataloader

Conserver les instances dans le Contexte le temps de la requête
const post = await context.loaders.post.load(postId);
const authors = comments && await context.loaders.comment.loadMany(post.commentIds);import DataLoader from 'dataloader';
function createLoader<T extends mongoose.Document>(model: mongoose.Model<T>) {
return new DataLoader(
async (ids: readonly ObjectId[]) => {
const documents = await model.find({
_id: {
$in: ids
}
} as FilterQuery<T>);
return ids.map(
id =>
documents.find(
doc => doc._id && doc._id.toString() === id.toString()
) || null
);
},
{
cacheKeyFn: key => key.toString()
}
);
}Data sources
- Injectés dans le contexte par Apollo (context.datasources)
- Cache : stocke les résultats par ids dans redis, avec un TTL

const post = await context.datasources.post.findOneById(postId, { ttl: 180 });
const comments = post && await context.datasources.comment.findManyByIds(
post.commentIds, { ttl: 60 }
);context.dataSources.post.deleteFromCacheById(id)export function buildDataSources() {
return {
user: new MongoDataSource<LeanDocument<IUserModel>, GraphQLAPIContext>(MongoUser.collection),
post: new MongoDataSource<LeanDocument<IPostModel>, GraphQLAPIContext>(MongoPost.collection),
library: new MongoDataSource<LeanDocument<ILibraryModel>, GraphQLAPIContext>(MongoLibrary.collection),
// …
};
}
export type GraphQLContextDataSources = ReturnType<typeof buildDataSources>;new ApolloServer({
// …
cache,
dataSources: buildDataSources
})Et pour aller plus loin…
import { DataSource, DataSourceConfig } from 'apollo-datasource';
import { KeyValueCache } from 'apollo-server-caching';
import GraphQLAPIContext from 'GraphqlAPI/context';
const RECENT_POSTS_KEY = 'recent_posts_';
export default class CustomDataSource extends DataSource<GraphQLAPIContext> {
private cache?: KeyValueCache<string>;
initialize(config: DataSourceConfig<GraphQLAPIContext>) {
this.cache = config.cache;
if (super.initialize) {
super.initialize(config);
}
}
async getRecentPostsIds(authorId: string): Promise<string[]> {
const postsJson = this.cache
? await this.cache.get(`${RECENT_POSTS_KEY}${authorId}`)
: undefined;
const storedPostsIds: string[] | undefined =
(postsJson && isValidJSON(postsJson) && JSON.parse(postsJson)) || undefined;
if (storedPostsIds) {
return storedPostsIds;
} else {
// look in database
const posts = await searchRecentPosts(authorId);
const postsIds = posts.map(post => post.id);
if (this.cache) {
this.cache.set(
`${RECENT_POSTS_KEY}${authorId}`,
JSON.stringify(postsIds),
{ ttl: 180 }
);
}
return postsIds;
}
}
async clearRecentPostsIds(authorId: string) {
if (this.cache) {
await this.cache.delete(
`${RECENT_POSTS_KEY}${authorId}`
);
}
}
}Merci !
Plus de détails dans les articles du blog
(… et sur Sportall)
22 - Optimisation d'une API GraphQL avec du cache serveur
By lonestone
22 - Optimisation d'une API GraphQL avec du cache serveur
- 281