Intermédiaire9 min de lecture

Relations plusieurs-à-plusieurs dans DynamoDB

Un étudiant s'inscrit à plusieurs cours ; un cours accueille plusieurs étudiants. En SQL, tu te tournes vers une table de jointure et un JOIN à trois tables.

DynamoDB n'a pas de jointures, la relation doit donc vivre dans les clés — et l'astuce est de stocker chaque arête d'inscription sous une forme que les deux côtés peuvent Query directement.

Ce guide déroule le problème étudiants ↔ cours de bout en bout : les modes d'accès, le pattern de qui les résout, un schéma de clés original que tu peux copier, et comment relire les deux directions sans jamais scanner la table.

Comment modéliser une relation plusieurs-à-plusieurs dans DynamoDB ?

DynamoDB n'a pas de jointures, tu modélises donc une relation plusieurs-à-plusieurs avec le pattern de : stocke chaque lien comme son propre item d'arête indexé par un côté, puis ajoute un GSI inversé qui échange les clés. Une seule arête, écrite une fois, répond ensuite à bas coût aux requêtes des deux directions.

  • Stocke chaque inscription comme son propre item d'arête, pas comme un attribut liste sur l'un ou l'autre côté.
  • Indexe l'arête par l'étudiant (PK = STU#…, SK = ENROLL#CRS#…) pour qu'un seul Query renvoie toute la liste de cours d'un étudiant.
  • Ajoute un inversé qui échange les rôles (GSI1PK = CRS#…) pour que la même arête réponde aussi à « qui est dans ce cours ? ».
  • Une arête, écrite une fois, se lit à bas coût dans les deux sens — c'est tout le jeu.

Formule d'abord les modes d'accès

La modélisation DynamoDB part des modes d'accès : tu décides des lectures avant de choisir le moindre nom d'attribut. Une relation plusieurs-à-plusieurs a presque toujours deux lectures symétriques en plus des recherches d'entités :

  • Obtenir le profil d'un étudiant, et lister chaque cours auquel cet étudiant est inscrit.
  • Obtenir les métadonnées d'un cours, et lister chaque étudiant inscrit à ce cours.
  • Rechercher une seule arête d'inscription — pour mettre à jour une note ou abandonner le cours.

La difficulté : les deux lectures de liste pointent dans des directions opposées à travers le même ensemble d'arêtes. Une conception naïve en sert une à bas coût et force un Scan pour l'autre — le piège exact couvert dans Query vs Scan.

Le travail consiste à faire des deux directions un seul Query.

Utilise le pattern de liste d'adjacence

La recommandation propre à DynamoDB pour les relations est la liste d'adjacence : modélise chaque relation comme un item dont la clé de partition est un point d'extrémité et la clé de tri l'autre.

AWS documente cela sur la page Best Practices for Managing Many-to-Many Relationships du Guide du développeur DynamoDB.

Pourquoi des clés et pas une seconde table ? Parce que la primitive que DynamoDB te donne est un Query contre une seule partition.

Un Query lit une plage contiguë de valeurs de clé de tri sous une seule clé de partition en une seule opération facturée — c'est la seule « jointure » que le moteur propose.

Pour obtenir une relation qui se lit à bas coût depuis les deux côtés, tu dupliques l'arête : écris-la une fois indexée par l'étudiant, puis utilise un index secondaire pour projeter la même arête indexée par le cours.

C'est le raisonnement des clés surchargées de la conception à table unique, appliqué à une relation au lieu d'une hiérarchie parent-enfant.

La forme est deux vues empilées de la même arête — la table de base indexée par l'étudiant, le GSI inversé indexé par le cours :

même arête, clés échangéesmême arête, clés échangéesGSI1 inversé clé par coursGSI1PK CRS#math204GSI1SK STU#a91GSI1PK CRS#cs101GSI1SK STU#a91Table de base clé par étudiantPK STU#a91SK ENROLL#CRS#math204PK STU#a91SK ENROLL#CRS#cs101

Chaque arête est écrite une fois sur la table de base et projetée dans le GSI avec ses clés échangées, si bien qu'un Query contre l'une ou l'autre partition lit la relation à bas coût.

La filiation remonte à l'article Amazon Dynamo de 2007 : la clé de partition est l'unité de distribution, et l'accès par clé unique est le chemin rapide.

Les relations dans DynamoDB sont un exercice consistant à plier des lectures plusieurs-à-plusieurs dans ce chemin rapide.

Travaille l'exemple : étudiants ↔ cours

Utilise une seule table avec des clés génériques, PK et SK, et encode le type d'entité dans la valeur. L'arête d'inscription en est le cœur :

PKSKattributes
STU#a91PROFILEname, year, major
STU#a91ENROLL#CRS#math204 enrolledOn, grade
STU#a91ENROLL#CRS#cs101enrolledOn, grade
CRS#math204METADATAtitle, credits, term
CRS#cs101METADATAtitle, credits, term

Un seul Query PK = "STU#a91" renvoie le profil de l'étudiant et chaque inscription en une seule lecture. Restreins-le avec SK begins_with "ENROLL#" pour n'obtenir que les arêtes de cours. Cela résout « lister les cours d'un étudiant ».

Mais « lister les étudiants d'un cours » pointe dans l'autre sens — et la table de base ne peut pas y répondre, car l'id de l'étudiant est dans la clé de partition, pas dans la clé de tri.

Ajoute un index secondaire global inversé qui échange les rôles. Donne aux items d'arête une paire générique GSI1PK/GSI1SK portant le cours du côté partition et l'étudiant du côté tri :

PKSKGSI1PKGSI1SK
STU#a91ENROLL#CRS#math204CRS#math204STU#a91
STU#b30ENROLL#CRS#math204CRS#math204STU#b30
STU#a91ENROLL#CRS#cs101CRS#cs101STU#a91

Maintenant, Query GSI1 WHERE GSI1PK = "CRS#math204" liste chaque étudiant de ce cours — la lecture que la table de base ne pouvait pas servir. Un seul item d'arête, écrit une fois, répond aux deux directions.

Ce doit être un GSI, pas un LSI : la partition du cours est entièrement différente de la partition de l'étudiant, et un LSI partage la clé de partition de la table de base.

L'index s'étend sur plusieurs partitions, il doit donc être global — voir GSI vs LSI.

Un piège : les GSI dans DynamoDB sont renseignés de façon asynchrone. Une inscription toute neuve peut mettre un instant à apparaître dans la direction CRS#….

Traite la lecture de la liste des inscrits comme — ce que le Guide du développeur signale explicitement pour les index secondaires globaux.

Écris et lis dans DynoTable

Écrire l'inscription revient à renseigner quatre attributs de clé plus les données propres à l'arête. La condition qui empêche un étudiant de s'inscrire deux fois au même cours est une garde attribute_not_exists(PK) sur la clé composite.

C'est exactement le genre de condition que tu peux assembler visuellement avec le Générateur d'expressions DynamoDB au lieu d'écrire à la main les ExpressionAttributeNames et les valeurs de placeholder.

Dans DynoTable, tu pointes un Query sur GSI1, tu règles GSI1PK = "CRS#math204", et la liste des inscrits revient sous forme de table que tu peux lire, trier et éditer sur place — les deux directions de la relation parcourables depuis un seul schéma.

Interrogation du GSI inversé dans DynoTable pour lister chaque étudiant inscrit à un cours.
Interrogation du GSI inversé dans DynoTable pour lister chaque étudiant inscrit à un cours.

Pièges et étapes suivantes

  • Ne stocke pas un côté comme un attribut liste. Un tableau courseIds sur l'item étudiant semble bien rangé jusqu'à ce qu'un cours ait besoin de sa liste d'inscrits, que le tableau atteigne le plafond de 400 Ko par item, ou que deux inscriptions entrent en compétition et s'écrasent l'une l'autre. Des items d'arête distincts passent à l'échelle et se mettent à jour indépendamment.
  • Garde les données d'arête sur l'arête. La grade et la enrolledOn de l'inscription appartiennent à l'item d'arête, pas dupliquées sur l'étudiant ou le cours — il y a exactement une ligne par paire (étudiant, cours) à mettre à jour.
  • Fais attention à la propagation du GSI. La direction de l'index inversé est à cohérence à terme, si bien qu'une lecture immédiatement après une inscription peut accuser un retard d'une fraction de seconde.
  • Ne projette que ce dont la liste des inscrits a besoin. Une projection KEYS_ONLY ou étroite garde le GSI petit quand la vue de la liste n'a besoin que des ids.

Pour aller plus loin sur les patterns environnants, lis conception à table unique pour les clés surchargées et GSI vs LSI pour savoir quand l'index inversé doit être global. Et pour partir de tes propres relations, l'outil de Single-Table Design gratuit transforme une liste de modes d'accès du type « lister les cours d'un étudiant / lister les étudiants d'un cours » en plan PK/SK/GSI avec des items d'exemple.

Puis télécharge DynoTable pour modéliser le schéma étudiants ↔ cours pour de vrai — écris les arêtes, construis la condition avec le Générateur d'expressions, et interroge les deux directions de la relation sans un seul scan. Et quand tu veux quand même la vue JOIN classique à trois tables, le SQL Workbench de DynoTable l'exécute sur tes tables en direct.

Mis à jour

Essaie cette conception de façon interactive

Esquisse tes entités et tes motifs d’accès dans l’outil gratuit de Single-Table Design DynamoDB — il suggère des modèles de clés PK/SK, prévisualise les collections d’items et montre quels motifs nécessitent un GSI.

Ouvrir l’outil de Single-Table Design