Fortgeschritten7 Min. Lesezeit

n:m-Beziehungen in DynamoDB

Eine Studentin schreibt sich in viele Kurse ein; ein Kurs hält viele Studierende. In SQL greifst du zur Join-Tabelle und einem vierfachen JOIN.

DynamoDB hat keine Joins, also muss die Beziehung in den Keys leben — und der Trick ist, jede Einschreibungs-Edge in einer Form zu speichern, die beide Seiten direkt Queryen können.

Dieser Guide spielt das Problem Studierende ↔ Kurse von Anfang bis Ende durch: die Zugriffsmuster, das Adjazenzlisten-Muster, das sie löst, ein eigenes Key-Schema zum Kopieren und wie du beide Richtungen zurückliest, ohne je die Tabelle zu scannen.

Wie modellierst du eine n:m-Beziehung in DynamoDB?

DynamoDB hat keine Joins, also modellierst du eine n:m-Beziehung mit dem Adjazenzlisten-Muster: Speichere jeden Link als eigenes Edge-Item, das nach einer Seite gekeyt ist, und füge dann einen invertierten GSI hinzu, der die Keys tauscht. Eine einzige Edge, einmal geschrieben, beantwortet dann Queries aus beiden Richtungen günstig.

  • Speichere jede Einschreibung als eigenes Edge-Item, nicht als Listenattribut auf einer der Seiten.
  • Keye die Edge nach der Studentin (PK = STU#…, SK = ENROLL#CRS#…), sodass eine Query die gesamte Kursliste einer Studentin zurückgibt.
  • Füge einen invertierten GSI hinzu, der die Rollen tauscht (GSI1PK = CRS#…), sodass dieselbe Edge auch "wer ist in diesem Kurs?" beantwortet.
  • Eine Edge, einmal geschrieben, billig in beide Richtungen gelesen — das ist das ganze Spiel.

Rahme zuerst die Zugriffsmuster

Modellierung in DynamoDB ist zugriffsmuster-first: du legst die Reads fest, bevor du auch nur einen Attributnamen wählst. Eine n:m-Beziehung hat fast immer zwei symmetrische Reads plus die Entitäts-Lookups:

  • Hol das Profil einer Studentin und liste jeden Kurs, in den sie eingeschrieben ist.
  • Hol die Metadaten eines Kurses und liste jeden in diesen Kurs eingeschriebenen Studierenden.
  • Schlag eine einzelne Einschreibungs-Edge nach — um eine Note zu aktualisieren oder den Kurs abzulegen.

Der Schmerz: die beiden List-Reads zeigen in entgegengesetzte Richtungen über dieselbe Menge an Edges. Ein naives Design bedient eine billig und erzwingt einen Scan für die andere — genau die Falle, behandelt in Query vs. Scan.

Die Aufgabe ist, beide Richtungen zu einer einzigen Query zu machen.

Verwende das Adjazenzlisten-Muster

Die eigene Empfehlung von DynamoDB für Beziehungen ist die Adjazenzliste: modelliere jede Beziehung als ein Item, dessen Partition Key der eine Endpunkt und dessen Sort Key der andere ist.

AWS dokumentiert das auf der Seite Best Practices for Managing Many-to-Many Relationships des DynamoDB Developer Guide.

Warum Keys und nicht eine zweite Tabelle? Weil das Primitive, das DynamoDB dir gibt, eine Query gegen eine einzelne Partition ist.

Eine Query liest einen zusammenhängenden Bereich von Sort-Key-Werten unter einem Partition Key in einer abgerechneten Operation — das ist der einzige "Join", den die Engine anbietet.

Um eine Beziehung zu bekommen, die von beiden Seiten billig liest, duplizierst du die Edge: schreib sie einmal nach der Studentin gekeyt, dann nutze einen Secondary Index, um dieselbe Edge nach dem Kurs gekeyt zu projizieren.

Das ist das überladene-Keys-Denken aus Single-Table Design, angewandt auf eine Beziehung statt auf eine Eltern-Kind-Hierarchie.

Die Form sind zwei gestapelte Sichten derselben Edge — die Basistabelle nach Studentin gekeyt, der invertierte GSI nach Kurs:

Invertierter GSI1 nach Kurs gekeytBasistabelle nach Studentin gekeytdieselbe Edge, getauschte Keysdieselbe Edge, getauschte KeysPK STU#a91SK ENROLL#CRS#math204PK STU#a91SK ENROLL#CRS#cs101GSI1PK CRS#math204GSI1SK STU#a91GSI1PK CRS#cs101GSI1SK STU#a91

Jede Edge wird einmal auf der Basistabelle geschrieben und mit getauschten Keys in den GSI projiziert, sodass eine Query gegen beide Partitionen die Beziehung billig liest.

Die Linie reicht zurück zum Amazon-Dynamo-Paper von 2007: der Partition Key ist die Verteilungseinheit, und Single-Key-Zugriff ist der schnelle Pfad.

Beziehungen in DynamoDB sind eine Übung darin, n:m-Reads in genau diesen schnellen Pfad zu biegen.

Spiel das Beispiel durch: Studierende ↔ Kurse

Verwende eine Tabelle mit generischen Keys, PK und SK, und kodiere den Entitätstyp in den Wert. Die Einschreibungs-Edge ist das Herzstück:

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

Eine einzige Query PK = "STU#a91" gibt das Profil der Studentin und jede Einschreibung in einem Read zurück. Engt sie mit SK begins_with "ENROLL#" ein, um nur die Kurs-Edges zu bekommen. Das löst "liste die Kurse einer Studentin".

Aber "liste die Studierenden eines Kurses" zeigt in die andere Richtung — und die Basistabelle kann es nicht beantworten, weil die Studierenden-id im Partition Key steckt, nicht im Sort Key.

Füge einen invertierten Global Secondary Index hinzu, der die Rollen tauscht. Gib den Edge-Items ein generisches GSI1PK/GSI1SK-Paar, das den Kurs auf der Partitionsseite und den Studierenden auf der Sortierseite hält:

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

Jetzt listet Query GSI1 WHERE GSI1PK = "CRS#math204" jeden Studierenden in diesem Kurs auf — der Read, den die Basistabelle nicht bedienen konnte. Ein Edge-Item, einmal geschrieben, beantwortet beide Richtungen.

Es muss ein GSI sein, kein LSI: die Kurs-Partition ist völlig verschieden von der Studierenden-Partition, und ein LSI teilt sich den Partition Key der Basistabelle.

Der Index spannt mehrere Partitionen, also muss er global sein — siehe GSI vs. LSI.

Ein Haken: GSIs in DynamoDB werden asynchron befüllt. Eine brandneue Einschreibung kann einen Moment brauchen, bis sie in der CRS#…-Richtung erscheint.

Behandle den Kurs-Roster-Read als letztendlich konsistent — was der Developer Guide für Global Secondary Indexes ausdrücklich hervorhebt.

Schreib und lies es in DynoTable

Die Einschreibung zu schreiben heißt, vier Key-Attribute plus die eigenen Daten der Edge zu setzen. Die Bedingung, die eine Studentin daran hindert, sich zweimal in denselben Kurs einzuschreiben, ist ein attribute_not_exists(PK)-Guard auf dem zusammengesetzten Key.

Das ist genau die Art Bedingung, die du visuell mit dem DynamoDB Expression Builder zusammenbauen kannst, statt die ExpressionAttributeNames und Platzhalterwerte von Hand zu schreiben.

In DynoTable richtest du eine Query auf GSI1, setzt GSI1PK = "CRS#math204", und der Roster kommt als Tabelle zurück, die du an Ort und Stelle lesen, sortieren und bearbeiten kannst — beide Richtungen der Beziehung aus einem Schema durchstöberbar.

Den invertierten GSI in DynoTable abfragen, um jeden in einen Kurs eingeschriebenen Studierenden aufzulisten.
Den invertierten GSI in DynoTable abfragen, um jeden in einen Kurs eingeschriebenen Studierenden aufzulisten.

Fallstricke und nächste Schritte

  • Speichere eine Seite nicht als Listenattribut. Ein courseIds-Array auf dem Studierenden-Item wirkt ordentlich, bis ein Kurs seinen Roster braucht, das Array die 400-KB-Item-Grenze erreicht oder zwei Einschreibungen kollidieren und sich gegenseitig überschreiben. Diskrete Edge-Items skalieren und aktualisieren unabhängig.
  • Halte Edge-Daten auf der Edge. Die grade und enrolledOn der Einschreibung gehören auf das Edge-Item, nicht dupliziert auf die Studentin oder den Kurs — es gibt genau eine Zeile pro Paar (Studentin, Kurs) zum Aktualisieren.
  • Bedenke die GSI-Propagierung. Die invertierte Indexrichtung ist letztendlich konsistent, also kann ein Read direkt nach einer Einschreibung um den Bruchteil einer Sekunde hinterherhinken.
  • Projiziere nur, was der Roster braucht. Ein KEYS_ONLY oder eine schmale Projektion hält den GSI klein, wenn die Roster-Sicht nur ids braucht.

Um tiefer in die umgebenden Muster einzusteigen, lies Single-Table Design für überladene Keys und GSI vs. LSI dafür, wann der invertierte Index global sein muss.

Dann lade DynoTable herunter, um das Schema Studierende ↔ Kurse wirklich zu modellieren — schreib die Edges, baue die Bedingung mit dem Expression Builder und frag beide Richtungen der Beziehung ohne einen einzigen Scan ab.

Aktualisiert