Im vorherigen Modul haben wir die Zugriffsmuster der Spielanwendung definiert. In diesem Modul entwerfen wir den Primärschlüssel für die DynamoDB-Tabelle und aktivieren die Kernzugriffsmuster.

Veranschlagte Zeit für das Modul: 20 Minuten


Beachten Sie beim Entwurf des Primärschlüssels für eine DynamoDB-Tabelle die folgenden bewährten Verfahren:

  • Beginnen Sie mit den verschiedenen Entitäten in Ihrer Tabelle. Wenn Sie mehrere verschiedene Arten von Daten in einer einzigen Tabelle speichern, wie z. B. Mitarbeiter, Abteilungen, Kunden und Bestellungen, stellen Sie sicher, dass Ihr Primärschlüssel über eine Möglichkeit verfügt, jede Entität eindeutig zu identifizieren und Kernaktionen für einzelne Elemente zu ermöglichen.
  • Verwenden Sie Präfixe zur Unterscheidung zwischen Entitätstypen. Die Verwendung von Präfixen zur Unterscheidung zwischen Entitätstypen kann Kollisionen verhindern und bei der Abfrage helfen. Wenn Sie beispielsweise sowohl Kunden als auch Mitarbeiter in derselben Tabelle haben, könnte der Primärschlüssel für einen Kunden CUSTOMER#<CUSTOMERID> und der Primärschlüssel für einen Mitarbeiter EMPLOYEE#<EMPLOYEEID> sein.
  • Konzentrieren Sie sich zunächst auf Einzelaktionen, und fügen Sie dann, wenn möglich, Mehrfachaktionen hinzu.. Bei einem Primärschlüssel ist es wichtig, dass Sie die Lese- und Schreiboptionen für ein einzelnes Element durch die Verwendung der APIs für einzelne Elemente erfüllen können: GetItem, PutItem, UpdateItem und DeleteItem. Möglicherweise können Sie Ihre Lesemuster für mehrere Elemente auch mit dem Primärschlüssel erfüllen, indem Sie Abfrage verwenden. Wenn nicht, können Sie einen Sekundärindex hinzufügen, um die Anwendungsfälle der Abfrage zu behandeln.

Mit diesen bewährten Verfahren im Hinterkopf entwerfen wir den Primärschlüssel für die Tabelle der Spielanwendung und führen einige grundlegende Aktionen durch.


  • Schritt 1. Entwerfen Sie den Primärschlüssel

    Betrachten wir die verschiedenen Entitäten, wie in der vorhergehenden Einführung vorgeschlagen wurde. Im Spiel haben wir die folgenden Entitäten:

    • Benutzer
    • Spiel
    • UserGameMapping

    Ein UserGameMapping ist ein Datensatz, der anzeigt, dass ein Benutzer einem Spiel beigetreten ist. Es besteht eine Viele-zu-Viele-Beziehung zwischen Benutzer und Spiel.

    Ein Viele-zu-Viele-Mapping ist in der Regel ein Hinweis darauf, dass Sie zwei Abfragemuster erfüllen wollen, und dieses Spiel ist keine Ausnahme. Wir haben ein Zugriffsmuster, das alle Benutzer finden muss, die einem Spiel beigetreten sind, sowie ein anderes Muster, um alle Spiele zu finden, die ein Benutzer gespielt hat.

    Wenn Ihr Datenmodell mehrere Entitäten mit Beziehungen untereinander hat, verwenden Sie in der Regel einen zusammengesetzten Primärschlüssel mit sowohl HASH- als auch RANGE-Werten. Der zusammengesetzte Primärschlüssel gibt uns die Abfragemöglichkeit auf dem HASH-Schlüssel, um eines der von uns benötigten Abfragemuster zu erfüllen. In der DynamoDB-Dokumentation wird der Partitionsschlüssel als HASH und der Sortierschlüssel als RANGE bezeichnet, und in diesem Handbuch verwenden wir die API-Terminologie austauschbar und insbesondere, wenn wir den Code oder das DynamoDB-JSON-Drahtprotokollformat besprechen.

    Die beiden anderen Dateneinheiten - Benutzer und Spiel - haben keine natürliche Eigenschaft für den RANGE-Wert, da die Zugriffsmuster auf einen Benutzer oder ein Spiel eine Schlüsselwert-Nachschlagefunktion darstellen. Da ein RANGE-Wert erforderlich ist, können wir einen Füllwert für den RANGE-Schlüssel bereitstellen.

    In diesem Sinne verwenden wir das folgende Muster für HASH- und RANGE-Werte für jeden Entitätstyp.

    Entität HASH RANGE
    Benutzer USER#<USERNAME> #METADATA#<USERNAME>
    Spiel GAME#<GAME_ID> #METADATA#<GAME_ID>
    UserGameMapping GAME#<GAME_ID> USER#<USERNAME>

    Gehen wir die vorangegangene Tabelle durch.

    Für die Benutzerentität ist der HASH-Wert USER#<USERNAME>. Beachten Sie, dass wir ein Präfix verwenden, um die Entität zu identifizieren und mögliche Kollisionen zwischen den Entitätstypen zu verhindern.

    Für den RANGE-Wert auf der Benutzerentität verwenden wir das statische Präfix #METADATA#, gefolgt vom Wert USERNAME. Für den RANGE-Wert ist es wichtig, dass wir einen Wert haben, der bekannt ist, wie zum Beispiel der BENUTZERNAME. Dadurch sind Einzelaktionen wie GetItem, PutItem und DeleteItem möglich.

    Wir wollen jedoch auch einen RANGE-Wert mit unterschiedlichen Werten für verschiedene Benutzereinheiten, um eine gleichmäßige Partitionierung zu ermöglichen, wenn wir diese Spalte als HASH-Schlüssel für einen Index verwenden. Aus diesem Grund fügen wir den BENUTZERNAMEN hinzu.

    Die Spielentität hat ein Primärschlüsseldesign, das dem Design der Benutzerentität ähnlich ist. Sie verwendet ein anderes Präfix (GAME#) und eine GAME_ID anstatt des BENUTZERNAMENS, aber die Prinzipien sind die gleichen.

    Schließlich verwendet UserGameMapping den gleichen HASH-Schlüssel wie die Spielentität. Dies ermöglicht es uns, nicht nur die Metadaten für ein Spiel, sondern auch alle Benutzer eines Spiels in einer einzigen Abfrage abzurufen. Wir verwenden dann die Benutzerentität für den RANGE-Schlüssel auf dem UserGameMapping, um festzustellen, welcher Benutzer einem bestimmten Spiel beigetreten ist.

    Im nächsten Schritt erstellen wir eine Tabelle mit diesem Primärschlüsseldesign. 

  • Schritt 2: Erstellen einer Tabelle

    Nachdem wir nun den Primärschlüssel entworfen haben, lassen Sie uns eine Tabelle erstellen.

    Der Code, den Sie in Schritt 3 von Modul 1 heruntergeladen haben, enthält ein Python-Skript im scripts/-Verzeichnis namens create_table.py. Der Inhalt des Python-Skripts folgt.

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.create_table(
            TableName='battle-royale',
            AttributeDefinitions=[
                {
                    "AttributeName": "PK",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "SK",
                    "AttributeType": "S"
                }
            ],
            KeySchema=[
                {
                    "AttributeName": "PK",
                    "KeyType": "HASH"
                },
                {
                    "AttributeName": "SK",
                    "KeyType": "RANGE"
                }
            ],
            ProvisionedThroughput={
                "ReadCapacityUnits": 1,
                "WriteCapacityUnits": 1
            }
        )
        print("Table created successfully.")
    except Exception as e:
        print("Could not create table. Error:")
        print(e)
    

    Das vorhergehende Skript verwendet die CreateTable-Operation mit Boto 3, dem AWS SDK für Python. Die Operation deklariert zwei Attributdefinitionen, bei denen es sich um typisierte Attribute handelt, die im Primärschlüssel verwendet werden sollen. Obwohl DynamoDB schemalos ist, müssen Sie die Namen und Typen von Attributen, die für Primärschlüssel verwendet werden, deklarieren. Die Attribute müssen auf jedem Element, das in die Tabelle geschrieben wird, enthalten sein und müssen daher beim Anlegen einer Tabelle angegeben werden.

    Da wir verschiedene Entitäten in einer einzigen Tabelle speichern, können wir keine Primärschlüssel-Attributnamen wie UserId verwenden. Das Attribut bedeutet etwas anderes, je nach Art der zu speichernden Entität. Zum Beispiel könnte der Primärschlüssel für einen Benutzer sein BENUTZERNAME sein, und der Primärschlüssel für ein Spiel könnte seine GAMEID sein. Dementsprechend verwenden wir generische Namen für die Attribute, wie z. B. PK (für den Partitionsschlüssel) und SK (für den Sortierschlüssel).

    Nachdem wir die Attribute im Schlüsselschema konfiguriert haben, geben wir den bereitgestellten Durchsatz für die Tabelle an. DynamoDB verfügt über zwei Kapazitätsmodi: bereitgestellt und On-Demand. Im Modus der bereitgestellten Kapazität geben Sie genau die Menge an Lese- und Schreibdurchsatz an, die Sie wünschen. Sie zahlen für diese Kapazität, ob Sie sie nutzen oder nicht.

    Im On-Demand-Kapazitätsmodus von DynamoDB können Sie pro Anfrage bezahlen. Die Kosten pro Anfrage sind etwas höher, als wenn Sie den bereitgestellten Durchsatz vollständig nutzen würden, aber Sie müssen keine Zeit für die Kapazitätsplanung aufwenden oder sich Sorgen machen, dass Sie gedrosselt werden. Der On-Demand-Modus eignet sich hervorragend für hohe oder unvorhersehbare Workloads. Wir verwenden in dieser Übung den Modus der bereitgestellten Kapazität, weil er in das kostenlose Kontingent von DynamoDB passt.

    Um die Tabelle zu erstellen, führen Sie das Python-Skript mit dem folgenden Befehl aus.

    python scripts/create_table.py

    Das Skript sollte diese Nachricht zurückgeben: "Tabelle erfolgreich erstellt".

    Im nächsten Schritt laden wir einige Beispieldaten in die Tabelle. 

  • Schritt 3: Laden Sie Daten in die Tabelle

    In diesem Schritt laden wir einige Daten in die DynamoDB, die wir im vorhergehenden Schritt erstellt haben. Das bedeutet, dass wir in den folgenden Schritten Beispieldaten zur Verfügung haben werden.

    Im Verzeichnis scripts/ finden Sie eine Datei namens items.json. Diese Datei enthält 835 Beispielelemente, die nach dem Zufallsprinzip für diese Übung generiert wurden. Diese Elemente umfassen die Entitäten Benutzer, Spiel und UserGameMapping. Öffnen Sie die Datei, wenn Sie einige der Beispielelemente sehen möchten.

    Im Verzeichnis scripts/ gibt es auch eine Datei namens bulk_load_table.py, die die Elemente in der Datei items.json liest und sie in die DynamoDB-Tabelle schreibt. Der Inhalt dieser Datei folgt.

    import json
    
    import boto3
    
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('battle-royale')
    
    items = []
    
    with open('scripts/items.json', 'r') as f:
        for row in f:
            items.append(json.loads(row))
    
    with table.batch_writer() as batch:
        for item in items:
            batch.put_item(Item=item)
    

    In diesem Skript verwenden wir statt des Low-Level-Clients in Boto 3 ein übergeordnetes Ressourcenobjekt. Ressourcenobjekte bieten eine einfachere Schnittstelle für die Verwendung der AWS-APIs. Das Ressourcen objekt ist in dieser Situation nützlich, weil es unsere Anfragen bündelt. Der Vorgang BatchWriteItem akzeptiert bis zu 25 Elemente in einer einzigen Anfrage. Das Ressourcenobjekt übernimmt diese Stapelung für uns, anstatt uns zu veranlassen, unsere Daten in Anfragen von 25 oder weniger Elementen zu unterteilen.

    Führen Sie das Skript bulk_load_table.py aus und laden Sie Ihre Tabelle mit Daten, indem Sie den folgenden Befehl im Terminal ausführen.

    python scripts/bulk_load_table.py

    Sie können sicherstellen, dass alle Ihre Daten in die Tabelle geladen wurden, indem Sie einen Scan-Vorgang durchführen und die Zählung erhalten.

    aws dynamodb scan \
     --table-name battle-royale \
     --select COUNT

    Dies sollte die folgenden Ergebnisse anzeigen.

    {
        "Count": 835, 
        "ScannedCount": 835, 
        "ConsumedCapacity": null
    }
    

    Sie sollten eine Anzahl von 835 sehen, die anzeigt, dass alle Ihre Elemente erfolgreich geladen wurden.

    Im nächsten Schritt zeigen wir, wie Sie mehrere Entitätstypen in einer einzigen Anfrage abrufen können, was die Gesamtanzahl der Netzwerkanfragen, die Sie in Ihrer Anwendung stellen, reduzieren und die Anwendungsleistung verbessern kann.

  • Schritt 4: Rufen Sie mehrere Entitätstypen in einer einzigen Anfrage ab

    Wie wir im vorigen Modul gesagt haben, sollten Sie die DynamoDB-Tabellen für die Anzahl der eingehenden Anfragen optimieren. Wir haben auch erwähnt, dass DynamoDB keine Joins hat, die eine relationale Datenbank hat. Stattdessen entwerfen Sie Ihre Tabelle so, dass ein Join-ähnliches Verhalten in Ihren Anfragen möglich ist.

    In diesem Schritt rufen wir mehrere Entitätstypen in einer einzigen Anfrage ab. Im Spiel wollen wir vielleicht Details über eine Spielsitzung abrufen. Zu diesen Details gehören Informationen über das Spiel selbst, wie z. B. die Zeit, zu der es begann, die Zeit, zu der es endete, wer darin platziert wurde, und Details über die Benutzer, die im Spiel gespielt haben.

    Diese Anfrage erstreckt sich auf zwei Entitätstypen: die Spielentität und die UserGameMapping-Entität. Das bedeutet jedoch nicht, dass wir mehrere Anfragen stellen müssen.

    In dem Code, den Sie heruntergeladen haben, befindet sich ein fetch_game_and_players.py-Skript im application/-Verzeichnis. Dieses Skript zeigt, wie Sie Ihren Code strukturieren können, um sowohl die Spielentität als auch die UserGameMapping-Entität für das Spiel in einer einzigen Anfrage abzurufen.

    Der folgende Code setzt das Skript fetch_game_and_players.py zusammen.

    import boto3
    
    from entities import Game, UserGameMapping
    
    dynamodb = boto3.client('dynamodb')
    
    GAME_ID = "3d4285f0-e52b-401a-a59b-112b38c4a26b"
    
    
    def fetch_game_and_users(game_id):
        resp = dynamodb.query(
            TableName='battle-royale',
            KeyConditionExpression="PK = :pk AND SK BETWEEN :metadata AND :users",
            ExpressionAttributeValues={
                ":pk": { "S": "GAME#{}".format(game_id) },
                ":metadata": { "S": "#METADATA#{}".format(game_id) },
                ":users": { "S": "USER$" },
            },
            ScanIndexForward=True
        )
    
        game = Game(resp['Items'][0])
        game.users = [UserGameMapping(item) for item in resp['Items'][1:]]
    
        return game
    
    
    game = fetch_game_and_users(GAME_ID)
    
    print(game)
    for user in game.users:
        print(user)
    

    Zu Beginn dieses Skripts importieren wir die Boto 3-Bibliothek und einige einfache Klassen, um die Objekte in unserem Anwendungscode darzustellen. Sie können die Definitionen für diese Entitäten in der Datei application/entities.py sehen.

    Die eigentliche Arbeit des Skripts geschieht in der Funktion fetch_game_and_users, die im Modul definiert ist. Dies ähnelt einer Funktion, die Sie in Ihrer Anwendung definieren würden und die von allen Endpunkten verwendet wird, die diese Daten benötigen.

    Die Funktion fetch_game_and_users erledigt so manche Sachen. Zunächst wird eine Abfrageanforderung an DynamoDB gestellt. Diese Anfrage Query benutze ein PK des Typs GAME#<GameId>. Dann fordert es alle Entitäten an, bei denen der Sortierschlüssel zwischen #METADATA#<GameId> und USER$ liegt. Dies bezieht die Spielentität, deren Sortierschlüssel #METADATA#<GameId> lautet, und alle UserGameMappings-Entitäten, deren Schlüssel mit USER# anfangen. Sortierschlüssel vom Typ String werden nach ASCII-Zeichencodes sortiert. Das Dollar-Zeichen ($) kommt in ASCII direkt nach dem Pfundzeichen (#), so dass sichergestellt ist, dass wir alle Mappings in der UserGameMapping-Entität erhalten.

    Wenn wir eine Antwort erhalten, setzen wir unsere Datenentitäten zu Objekten zusammen, die unserer Anwendung bekannt sind. Wir wissen, dass die erste zurückgegebene Entität die Entität Game ist, also erstellen wir ein Game-Objekt aus der Entität. Für die übrigen Entitäten erstellen wir für jede Entität ein UserGameMapping-Objekt und hängen dann das Array der Benutzer an das Spiel-Objekt an.

    Das Ende des Skripts zeigt die Verwendung der Funktion und druckt die resultierenden Objekte aus. Sie können dieses Skript mit dem folgenden Befehl in Ihrem Terminal ausführen:

    python application/fetch_game_and_players.py

    Das Skript sollte das Spiel-Objekt und alle UserGameMapping-Objekte auf die Konsole drucken.

    Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- branchmichael>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- deanmcclure>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- emccoy>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- emma83>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- iherrera>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- jeremyjohnson>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- lisabaker>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- maryharris>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- mayrebecca>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- meghanhernandez>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- nruiz>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- pboyd>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- richardbowman>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- roberthill>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- robertwood>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- victoriapatrick>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- waltervargas>
    

    Dieses Skript zeigt, wie Sie Ihre Tabelle modellieren und Ihre Abfragen schreiben können, um mehrere Entitätstypen in einer einzigen DynamoDB-Anfrage abzurufen. In einer relationalen Datenbank verwenden Sie Joins, um mehrere Entitätstypen aus verschiedenen Tabellen in einer einzigen Anfrage abzurufen. Mit DynamoDB modellieren Sie Ihre Daten gezielt so, dass Entitäten, auf die Sie gemeinsam zugreifen sollen, nebeneinander in einer einzigen Tabelle liegen. Dieser Ansatz ersetzt die Notwendigkeit von Joins in einer typischen relationalen Datenbank und hält Ihre Anwendung bei der Skalierung hoch performant.


    In diesem Modul haben wir einen Primärschlüssel entworfen und eine Tabelle erstellt. Dann luden wir Daten in die Tabelle und sahen, wie man in einer einzigen Anfrage nach mehreren Entitätstypen abfragt.

    Mit dem derzeitigen Primärschlüsseldesign können wir die folgenden Zugriffsmuster erfüllen:

    • Benutzerprofil erstellen (Schreibvorgang)
    • Benutzerprofil aktualisieren (Schreibvorgang)
    • Benutzerprofil abrufen (Lesevorgang)
    • Spiel erstellen (Schreibvorgang)
    • Spiel ansehen (Lesevorgang)
    • Dem Spiel für einen Benutzer beitreten (Schreibvorgang)
    • Spiel starten (Schreibvorgang)
    • Spiel für einen Benutzer aktualisieren (Schreibvorgang)
    • Spiel aktualisieren (Schreibvorgang)

    Im nächsten Modul fügen wir einen Sekundärindex hinzu und lernen die Technik des spärlichen Index kennen. Mit Sekundärindizes können Sie zusätzliche Zugriffsmuster auf Ihre DynamoDB-Tabelle unterstützen.