Eine der größten Anpassungen für Benutzer, die neu in DynamoDB und NoSQL sind, ist die Modellierung von Daten, die über einen gesamten Datensatz gefiltert werden sollen. In unserem Spiel müssen wir zum Beispiel Spielsitzungen mit offenen Stellen finden, damit wir den Benutzern zeigen können, an welcher Spielsitzung sie teilnehmen können.
In einer relationalen Datenbank würden Sie etwas SQL schreiben, um die Daten abzufragen.

SELECT * FROM games
	WHERE status = “OPEN”

DynamoDB kann die Ergebnisse eines Abfrage- oder Scanvorgangs filtern, aber DynamoDB funktioniert nicht wie eine relationale Datenbank. Ein DynamoDB-Filter wird angewendet, nachdem die anfänglichen Elemente, die dem Abfrage- oder Scanvorgang entsprechen, abgerufen wurden. Der Filter reduziert die Größe der vom DynamoDB-Dienst gesendeten Nutzlast, aber die Anzahl der anfänglich abgerufenen Elemente unterliegt den Größenbeschränkungen von DynamoDB.

Glücklicherweise gibt es eine Reihe von Möglichkeiten, wie Sie gefilterte Abfragen gegen Ihren Datenbestand in DynamoDB zulassen können. Um effiziente Filter auf Ihrer DynamoDB-Tabelle bereitzustellen, müssen Sie die Filter von Anfang an in das Datenmodell Ihrer Tabelle einplanen. Erinnern Sie sich an die Lektion, die wir im zweiten Modul dieser Übung gelernt haben: Überlegen Sie sich Ihre Zugriffsmuster und entwerfen Sie dann Ihre Tabelle.

In den folgenden Schritten verwenden wir einen globalen Sekundärindex, um offene Spiele zu finden. Insbesondere werden wir die spärliche Index-Technik zur Handhabung dieses Zugriffsmusters verwenden.

Veranschlagte Zeit für das Modul: 40 Minuten


  • Schritt 1: Modellieren Sie einen spärlichen Sekundärindex

    Sekundärindizes sind wichtige Datenmodellierungswerkzeuge in DynamoDB. Sie ermöglichen es Ihnen, Ihre Daten umzuformen, um alternative Abfragemuster zu ermöglichen. Um einen Sekundärindex anzulegen, geben Sie den Primärschlüssel des Index an, genau wie beim vorherigen Anlegen einer Tabelle. Beachten Sie, dass der Primärschlüssel für einen globalen Sekundärindex nicht für jedes Element eindeutig sein muss. DynamoDB kopiert dann Elemente in den Index, basierend auf den angegebenen Attributen, und Sie können sie genau wie die Tabelle abfragen.

    Die Verwendung spärlicher Sekundärindizes ist eine fortschrittliche Strategie in DynamoDB. Bei Sekundärindizes kopiert DynamoDB Elemente aus der Originaltabelle nur dann, wenn sie die Elemente des Primärschlüssels im Sekundärindex haben. Elemente, die nicht die primären Schlüsselelemente haben, werden nicht kopiert, weshalb diese Sekundärindizes als "spärlich" bezeichnet werden.

    Lasst uns sehen, wie sich das auf uns auswirkt. Sie erinnern sich vielleicht daran, dass wir zwei Zugangsmuster haben, um offene Spiele zu finden:

    • Offene Spiele finden (Lesevorgang)
    • Offene Spiele nach Karte finden (Lesevorgang)

    Wir können einen Sekundärindex unter Verwendung eines zusammengesetzten Primärschlüssels erstellen, wobei der HASH-Schlüssel das Map-Attribut für das Spiel und der RANGE-Schlüssel das open_timestamp-Attribut für das Spiel ist, das die Zeit angibt, zu der das Spiel geöffnet wurde.

    Der wichtige Teil für uns ist, dass das open_timestamp-Attribut gelöscht wird, wenn ein Spiel voll wird. Wenn das Attribut gelöscht wird, wird das gefüllte Spiel aus dem Sekundärindex entfernt, weil es keinen Wert für das Schlüsselattribut RANGE hat. Das ist es, was unseren Index spärlich hält: Er enthält nur offene Spiele, die das open_timestamp-Attribut haben.

    Im nächsten Schritt erstellen wir den Sekundärindex.

  • Schritt 2: Erstellen Sie einen spärlichen Sekundärindex

    In diesem Schritt erstellen wir den spärlichen Sekundärindex für offene Spiele (Spiele, die nicht bereits voll sind).

    Das Anlegen eines Sekundärindexes ist dem Anlegen einer Tabelle ähnlich. In dem Code, den Sie heruntergeladen haben, finden Sie eine Skriptdatei im Verzeichnis scripts/ namens add_secondary_index.py. Der Inhalt dieser Datei ist der folgende.

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.update_table(
            TableName='battle-royale',
            AttributeDefinitions=[
                {
                    "AttributeName": "map",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "open_timestamp",
                    "AttributeType": "S"
                }
            ],
            GlobalSecondaryIndexUpdates=[
                {
                    "Create": {
                        "IndexName": "OpenGamesIndex",
                        "KeySchema": [
                            {
                                "AttributeName": "map",
                                "KeyType": "HASH"
                            },
                            {
                                "AttributeName": "open_timestamp",
                                "KeyType": "RANGE"
                            }
                        ],
                        "Projection": {
                            "ProjectionType": "ALL"
                        },
                        "ProvisionedThroughput": {
                            "ReadCapacityUnits": 1,
                            "WriteCapacityUnits": 1
                        }
                    }
                }
            ],
        )
        print("Table updated successfully.")
    except Exception as e:
        print("Could not update table. Error:")
        print(e)

    Wann immer Attribute in einem Primärschlüssel für eine Tabelle oder einen Sekundärindex verwendet werden, müssen sie in AttributeDefinitions definiert werden. Dann erstellen wir einen neuen Sekundärindex in der Eigenschaft GlobalSecondaryIndexUpdates. Für diesen Sekundärindex geben wir den Indexnamen, das Schema des Primärschlüssels, den bereitgestellten Durchsatz und die Attribute an, die wir projizieren wollen.

    Beachten Sie, dass wir nicht angeben mussten, dass unser Sekundärindex als spärlicher Index verwendet werden soll. Das ist eine reine Funktion der von Ihnen eingegebenen Daten. Wenn Sie Elemente in Ihre Tabelle schreiben, die nicht die Attribute für Ihre Sekundärindizes haben, werden sie nicht in Ihren Sekundärindex aufgenommen.

    Erstellen Sie Ihren Sekundärindex, indem Sie den folgenden Befehl ausführen.

    python scripts/add_secondary_index.py

    In der Konsole sollten Sie folgende Meldung sehen: "Tabelle erfolgreich aktualisiert".

    Im nächsten Schritt verwenden wir den spärlichen Index, um offene Spiele nach Karte zu finden.

  • Schritt 3: Abfrage des spärlichen Sekundärindexes

    Nachdem wir nun den Sekundärindex konfiguriert haben, wollen wir ihn nutzen, um einige der Zugriffsmuster zu befriedigen.

    Um einen Sekundärindex zu verwenden, stehen Ihnen zwei API-Aufrufe zur Verfügung: Abfrage und Scan. Bei Abfrage müssen Sie den HASH-Schlüssel angeben, und es liefert ein gezieltes Ergebnis. Bei Scan geben Sie keinen HASH-Schlüssel an, und der Vorgang läuft über Ihre gesamte Tabelle hinweg. Von Scans in DynamoDB wird außer unter bestimmten Umständen abgeraten, da sie auf jedes Element in Ihrer Datenbank zugreifen. Wenn Sie eine beträchtliche Menge an Daten in Ihrer Tabelle haben, kann das Scannen sehr lange dauern. Im nächsten Schritt zeigen wir Ihnen, warum Scans ein leistungsfähiges Werkzeug sein können, wenn sie mit spärlichen Indizes verwendet werden.

    Wir können die Abfrage-API anhand des Sekundärindexes verwenden, den wir im vorherigen Schritt erstellt haben, um alle offenen Spiele nach Kartennamen zu finden. Der Sekundärindex ist nach Kartennamen gegliedert, so dass wir gezielt nach offenen Spielen suchen können.

    In dem Code, den Sie heruntergeladen haben, ist eine find_open_games_by_map.py-Datei im application/ -Verzeichnis enthalten. Der Inhalt dieses Skripts folgt.

    import boto3
    
    from entities import Game
    
    dynamodb = boto3.client('dynamodb')
    
    def find_open_games_by_map(map_name):
        resp = dynamodb.query(
            TableName='battle-royale',
            IndexName="OpenGamesIndex",
            KeyConditionExpression="#map = :map",
            ExpressionAttributeNames={
                "#map": "map"
            },
            ExpressionAttributeValues={
                ":map": { "S": map_name },
            },
            ScanIndexForward=True
        )
    
        games = [Game(item) for item in resp['Items']]
    
        return games
    
    games = find_open_games_by_map("Green Grasslands")
    for game in games:
        print(game)

    Im vorhergehenden Skript ähnelt die Funktion find_open_games_by_map einer Funktion, die Sie in Ihrer Anwendung haben würden. Die Funktion akzeptiert einen Kartennamen und stellt eine Abfrage an den OpenGamesIndex, um alle offenen Spiele für die Karte zu finden. Anschließend werden die zurückgegebenen Entitäten zu Spielobjekten zusammengesetzt, die in Ihrer Anwendung verwendet werden können.

    Führen Sie dieses Skript aus, indem Sie den folgenden Befehl in Ihrem Terminal ausführen.

    python application/find_open_games_by_map.py

    Das Terminal zeigt die folgende Ausgabe mit vier offenen Spielen für die Karte "Grünes Grasland".

    Open games for Green Grasslands:
    Game<14c7f97e-8354-4ddf-985f-074970818215 -- Green Grasslands>
    Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands>
    Game<683680f0-02b0-4e5e-a36a-be4e00fc93f3 -- Green Grasslands>
    Game<0ab37cf1-fc60-4d93-b72b-89335f759581 -- Green Grasslands>
    sudo cp -r wordpress/* /var/www/html/

    Im nächsten Schritt verwenden wir die Scan-API, um den spärlichen Sekundärindex zu scannen.

  • Schritt 4: Scannen des spärlichen Sekundärindexes

    Im vorherigen Schritt haben wir gesehen, wie man Spiele für eine bestimmte Karte findet. Einige Spieler ziehen es vielleicht vor, auf einer bestimmten Karte zu spielen, daher ist dies nützlich. Andere Spieler können bereit sein, ein Spiel auf jeder Karte zu spielen. In diesem Abschnitt zeigen wir, wie man jedes offene Spiel in der Anwendung findet, unabhängig von der Art der Karte. Dazu verwenden wir die Scan-API.

    Im Allgemeinen möchten Sie Ihre Tabelle nicht für die Verwendung des DynamoDB-Scan-Vorgangs entwerfen, da DynamoDB für präzise Abfragen gebaut ist, die genau die Entitäten erfassen, die Sie benötigen. Ein Scan-Vorgang greift auf eine zufällige Sammlung von Entitäten in Ihrer Tabelle zu, so dass die Suche nach den benötigten Entitäten mehrere Datenbank-Durchläufe erfordern kann.

    Manchmal kann ein Scan jedoch nützlich sein. In unserer Situation haben wir einen spärlichen Sekundärindex, was bedeutet, dass unser Index nicht so viele Entitäten enthalten sollte. Darüber hinaus enthält der Index nur die Spiele, die offen sind, und das ist genau das, was wir brauchen.

    Für diesen Anwendungsfall funktioniert der Scan hervorragend. Mal sehen, wie es funktioniert. In dem Code, den Sie heruntergeladen haben, ist eine find_open_games.py-Datei im application/-Verzeichnis enthalten. Der Inhalt der Datei folgt.

    import boto3
    
    from entities import Game
    
    dynamodb = boto3.client('dynamodb')
    
    def find_open_games():
        resp = dynamodb.scan(
            TableName='battle-royale',
            IndexName="OpenGamesIndex",
        )
    
        games = [Game(item) for item in resp['Items']]
    
        return games
    
    games = find_open_games()
    print("Open games:")
    for game in games:
        print(game)

    Dieser Code ist ähnlich wie der Code im vorherigen Schritt. Anstatt jedoch die query()-Methode auf dem DynamoDB-Client zu verwenden, verwenden wir die scan()-Methode. Da wir scan() verwenden, müssen wir nichts über die Schlüsselbedingungen angeben, wie wir es bei query() getan haben. Wir lassen DynamoDB nur einen Haufen von Elementen in keiner bestimmten Reihenfolge zurücksenden.

    Führen Sie das Skript mit dem folgenden Befehl in Ihrem Terminal aus.

    python application/find_open_games.py

    Ihr Terminal sollte eine Liste von neun Spielen ausdrucken, die auf verschiedenen Karten offen sind.

    Open games:
    Game<c6f38a6a-d1c5-4bdf-8468-24692ccc4646 -- Urban Underground>
    Game<d06af94a-2363-441d-a69b-49e3f85e748a -- Dirty Desert>
    Game<873aaf13-0847-4661-ba26-21e0c66ebe64 -- Dirty Desert>
    Game<fe89e561-8a93-4e08-84d8-efa88bef383d -- Dirty Desert>
    Game<248dd9ef-6b17-42f0-9567-2cbd3dd63174 -- Juicy Jungle>
    Game<14c7f97e-8354-4ddf-985f-074970818215 -- Green Grasslands>
    Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands>
    Game<683680f0-02b0-4e5e-a36a-be4e00fc93f3 -- Green Grasslands>
    Game<0ab37cf1-fc60-4d93-b72b-89335f759581 -- Green Grasslands>
    

    In diesem Schritt haben wir gesehen, wie die Verwendung des Scan-Vorgangs unter bestimmten Umständen die richtige Wahl sein kann. Wir nutzten Scan, um eine Auswahl von Entitäten aus unserem spärlichen Sekundärindex zu entnehmen, um den Spielern offene Spiele zu zeigen.

    In den nächsten Schritten erfüllen wir zwei Zugangsmuster:

    • Dem Spiel für einen Benutzer beitreten (Schreibvorgang)
    • Spiel starten (Schreibvorgang)

    Um das Zugriffsmuster "Dem Spiel für einen Benutzer beitreten" in den folgenden Schritten zu erfüllen, werden wir DynamoDB-Transaktionen verwenden. Transaktionen sind in relationalen Systemen für Vorgänge beliebt, die mehrere Datenelemente gleichzeitig betreffen. Stellen Sie sich zum Beispiel vor, Sie leiten eine Bank. Eine Kundin, Alejandra, überweist 100 USD an eine andere Kundin, Ana. Bei der Aufzeichnung dieser Transaktion würden Sie eine Transaktion verwenden, um sicherzustellen, dass die Änderungen auf die Salden beider Kunden angewendet werden und nicht nur auf einen.

    DynamoDB-Transaktionen erleichtern die Erstellung von Anwendungen, die mehrere Elemente als Teil eines einzigen Vorgangs ändern. Bei Transaktionen können Sie bis zu 10 Artikel als Teil einer einzigen Transaktionsanforderung bearbeiten.

    In einem TransactWriteItem-API-Aufruf können Sie die folgenden Vorgänge verwenden:

    • Setzen: Zum Einfügen oder Überschreiben eines Elements.
    • Aktualisieren Sie: Zum Aktualisieren eines vorhandenen Elements.
    • Löschen: Zum Entfernen eines Elements.
    • ConditionCheck: Für die Durchsetzung einer Bedingung auf ein bestehendes Element, ohne das Element zu verändern.

     

    Im nächsten Schritt verwenden wir eine DynamoDB-Transaktion, wenn wir neue Benutzer zu einem Spiel hinzufügen und verhindern gleichzeitig, dass das Spiel überfüllt wird.

  • Schritt 5: Benutzer zu einem Spiel hinzufügen

    Das erste Zugriffsmuster, das wir in diesem Modul ansprechen, ist das Hinzufügen neuer Benutzer zu einem Spiel.

    Wenn wir einen neuen Benutzer zu einem Spiel hinzufügen, müssen wir Folgendes tun:

    • bestätigen, dass nicht bereits 50 Spieler im Spiel sind (jedes Spiel kann maximal 50 Spieler haben).
    • bestätigen, dass der Benutzer nicht bereits im Spiel ist.
    • eine neue UserGameMapping-Entität erstellen, um den Benutzer zum Spiel hinzuzufügen.
    • das Attribut people auf der Spielentität erhöhen, um zu verfolgen, wie viele Spieler im Spiel sind.

    Beachten Sie, dass das Erreichen all dieser Dinge Schreibvorgänge über die bestehende Spielentität und die neue UserGameMapping -Entität sowie eine bedingte Logik für jede der Entitäten erfordert. Dies ist die Art von Vorgang, die perfekt für DynamoDB-Transaktionen geeignet ist, da Sie an mehreren Entitäten in der gleichen Anfrage arbeiten müssen und Sie wollen, dass die gesamte Anfrage zusammen erfolgreich ist oder fehlschlägt.

    In dem Code, den Sie heruntergeladen haben, befindet sich ein join_game.py-Skript im Verzeichnis application/. Die Funktion in diesem Skript verwendet eine DynamoDB-Transaktion, um einen Benutzer zu einem Spiel hinzuzufügen.

    Der Inhalt des Skripts folgt.

    import boto3
    
    from entities import Game, UserGameMapping
    
    dynamodb = boto3.client('dynamodb')
    
    GAME_ID = "c6f38a6a-d1c5-4bdf-8468-24692ccc4646"
    USERNAME = 'vlopez'
    
    
    def join_game_for_user(game_id, username):
        try:
            resp = dynamodb.transact_write_items(
                TransactItems=[
                    {
                        "Put": {
                            "TableName": "battle-royale",
                            "Item": {
                                "PK": {"S": "GAME#{}".format(game_id) },
                                "SK": {"S": "USER#{}".format(username) },
                                "game_id": {"S": game_id },
                                "username": {"S": username }
                            },
                            "ConditionExpression": "attribute_not_exists(SK)",
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
                        },
                    },
                    {
                        "Update": {
                            "TableName": "battle-royale",
                            "Key": {
                                "PK": { "S": "GAME#{}".format(game_id) },
                                "SK": { "S": "#METADATA#{}".format(game_id) },
                            },
                            "UpdateExpression": "SET people = people + :p",
                            "ConditionExpression": "people <= :limit",
                            "ExpressionAttributeValues": {
                                ":p": { "N": "1" },
                                ":limit": { "N": "50" }
                            },
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
                        }
                    }
                ]
            )
            print("Added {} to game {}".format(username, game_id))
            return True
        except Exception as e:
            print("Could not add user to game")
    
    join_game_for_user(GAME_ID, USERNAME)

    In der join_game_for_user-Funktion dieses Skripts führt die Methode transact_write_items() eine Schreibtransaktion aus. Diese Transaktion hat zwei Vorgänge.

    Im ersten Vorgang der Transaktion verwenden wir einen Setzen-Vorgang, um eine neue UserGameMapping-Entität einzufügen. Als Teil dieses Vorgangs geben wir eine Bedingung an, dass das SK-Attribut für diese Entität nicht existieren darf. Dadurch wird sichergestellt, dass eine entität mit dieser PK und SK nicht bereits existiert. Wenn eine solche Instanz bereits existiert, würde dies bedeuten, dass dieser Benutzer bereits dem Spiel beigetreten ist.

    Die zweite Operation ist ein Aktualisieren-Vorgang auf die Spielentität, um das Attribut people um eins zu erhöhen. Als Teil dieses Vorgangs fügen wir eine bedingte Überprüfung hinzu, dass der aktuelle Wert für people (Personen) nicht größer als 50 ist. Sobald 50 Personen an einem Spiel teilnehmen, ist das Spiel voll und kann beginnen.

    Führen Sie dieses Skript mit dem folgenden Befehl in Ihrem Terminal aus:

    python application/join_game.py

    Die Ausgabe in Ihrem Terminal sollte anzeigen, dass der Benutzer zum Spiel hinzugefügt wurde.

    Added vlopez to game c6f38a6a-d1c5-4bdf-8468-24692ccc4646

    Beachten Sie, dass die Funktion fehlschlägt, wenn Sie versuchen, das Skript erneut auszuführen. Benutzer vlopez wurde dem Spiel bereits hinzugefügt, so dass der Versuch, den Benutzer erneut hinzuzufügen, die von uns angegebenen Bedingungen nicht erfüllt.

    Das Hinzufügen von DynamoDB-Transaktionen vereinfacht den Arbeitsablauf bei komplexen Vorgängen wie diesen erheblich. Ohne Transaktionen hätte dies mehrere API-Aufrufe mit komplexen Bedingungen und manuelle Rollbacks im Falle von Konflikten erfordert. Jetzt können wir solche komplexen Vorgänge mit weniger als 50 Codezeilen implementieren.

    Im nächsten Schritt behandeln wir das Zugriffsmuster "Spiel starten (Schreibvorgang)".

  • Schritt 6: Spiel starten

    Sobald ein Spiel 50 Benutzer hat, kann der Schöpfer des Spiels das Spiel starten, um den Spielverlauf zu initiieren. In diesem Schritt zeigen wir, wie man mit diesem Zugriffsmuster umgeht.

    Wenn unser Anwendungs-Backend eine Anfrage zum Start des Spiels erhält, überprüfen wir drei Dinge:

    • Für das Spiel haben sich 50 Personen angemeldet.
    • Der anfragende Benutzer ist der Schöpfer des Spiels.
    • Das Spiel hat noch nicht begonnen.

    Wir können jede dieser Prüfungen in einem Bedingungsausdruck in einer Anfrage zur Aktualisierung des Spiels behandeln. Wenn alle diese Prüfungen bestanden sind, müssen wir unsere entität auf folgende Weise aktualisieren:

    • Entfernen Sie das open_timestamp-Attribut, damit es nicht als offenes Spiel im spärlichen Sekundärindex aus dem vorherigen Modul erscheint.
    • Fügen Sie ein start_time-Attribut hinzu, das angibt, wann das Spiel gestartet wurde.

    In dem Code, den Sie heruntergeladen haben, befindet sich ein start_game.py-Skript im Verzeichnis application/. Der Inhalt der Datei lautet wie folgt:

    import datetime
    
    import boto3
    
    from entities import Game
    
    dynamodb = boto3.client('dynamodb')
    
    GAME_ID = "c6f38a6a-d1c5-4bdf-8468-24692ccc4646"
    CREATOR = "gstanley"
    
    def start_game(game_id, requesting_user, start_time):
        try:
            resp = dynamodb.update_item(
                TableName='battle-royale',
                Key={
                    "PK": { "S": "GAME#{}".format(game_id) },
                    "SK": { "S": "#METADATA#{}".format(game_id) }
                },
                UpdateExpression="REMOVE open_timestamp SET start_time = :time",
                ConditionExpression="people = :limit AND creator = :requesting_user AND attribute_not_exists(start_time)",
                ExpressionAttributeValues={
                    ":time": { "S": start_time.isoformat() },
                    ":limit": { "N": "50" },
                    ":requesting_user": { "S": requesting_user }
                },
                ReturnValues="ALL_NEW"
            )
            return Game(resp['Attributes'])
        except Exception as e:
            print('Could not start game')
            return False
    
    game = start_game(GAME_ID, CREATOR, datetime.datetime(2019, 4, 16, 10, 15, 35))
    
    if game:
        print("Started game: {}".format(game))

    In diesem Skript ähnelt die Funktion start_game der Funktion, die Sie in Ihrer Anwendung haben würden. Man benötigt game_id, requesting_user und start_time und führt eine Anforderung zur Aktualisierung der Spielentität aus, um das Spiel zu starten.

    Der ConditionExpression-Parameter im update_item()-Aufruf gibt jede der drei Prüfungen an, die wir zuvor in diesem Schritt aufgeführt haben. Das Spiel muss 50 Personen haben, der Benutzer, der den Spielstart anfordert, muss der Ersteller des Spiels sein, und das Spiel darf kein start_time-Attribut haben, das angibt, dass es bereits gestartet wurde.

    Im UpdateExpression-Parameter können Sie die Änderungen sehen, die wir an unserer Entität vornehmen wollen. Zuerst entfernen wir das open_timestamp-Attribut aus der Entität, und dann setzen wir das start_time-Attribut auf die Startzeit des Spiels.

    Führen Sie dieses Skript mit dem folgenden Befehl in Ihrem Terminal aus:

    python application/start_game.py

    Sie sollten eine Ausgabe in Ihrem Terminal sehen, die anzeigt, dass das Spiel erfolgreich gestartet wurde.

    Started game: Game<c6f38a6a-d1c5-4bdf-8468-24692ccc4646 -- Urban Underground>

    Versuchen Sie, das Skript ein zweites Mal in Ihrem Terminal auszuführen. Dieses Mal sollten Sie eine Fehlermeldung sehen, die anzeigt, dass Sie das Spiel nicht starten konnten. Das liegt daran, dass Sie das Spiel bereits gestartet haben, so dass das Attribut start_time vorhanden ist. Infolgedessen scheiterte die Anfrage bei der bedingten Prüfung der Entität.

    Sie erinnern sich vielleicht daran, dass es eine Viele-zu-Viele-Beziehung zwischen der Spielentität und den zugehörigen Benutzerentitäten gibt, und die Beziehung wird durch eine UserGameMapping-entität dargestellt.

    Häufig möchte man beide Seiten einer Beziehung abfragen. Mit unserem Primärschlüssel-Setup können wir alle Benutzerentitäten in einem Spiel finden. Wir können die Abfrage aller Spielentitäten für einen Benutzer aktivieren, indem wir einen invertierten Index verwenden.

    In DynamoDB ist ein invertierter Index ein Sekundärindex, der die Umkehrung Ihres Primärschlüssels ist. Der RANGE-Schlüssel wird zu Ihrem HASH-Schlüssel und umgekehrt. Dieses Muster dreht Ihre Tabelle um und ermöglicht Ihnen die Abfrage auf der anderen Seite Ihrer Viele-zu-Viele-Beziehungen.

    In den folgenden Schritten fügen wir der Tabelle einen invertierten Index hinzu und zeigen, wie man ihn verwendet, um alle Spielentitäten für einen bestimmten Benutzer abzurufen. 

  • Schritt 7: Einen invertierten Index hinzufügen

    In diesem Schritt fügen wir der Tabelle einen invertierten Index hinzu. Ein invertierter Index wird wie jeder andere Sekundärindex erstellt.

    In dem Code, den Sie heruntergeladen haben, befindet sich ein add_inverted_index.py-Skript im Verzeichnis scripts/. Dieses Python-Skript fügt einen invertierten Index zu Ihrer Tabelle hinzu.

    Der Inhalt dieser Datei lautet wie folgt:

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.update_table(
            TableName='battle-royale',
            AttributeDefinitions=[
                {
                    "AttributeName": "PK",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "SK",
                    "AttributeType": "S"
                }
            ],
            GlobalSecondaryIndexUpdates=[
                {
                    "Create": {
                        "IndexName": "InvertedIndex",
                        "KeySchema": [
                            {
                                "AttributeName": "SK",
                                "KeyType": "HASH"
                            },
                            {
                                "AttributeName": "PK",
                                "KeyType": "RANGE"
                            }
                        ],
                        "Projection": {
                            "ProjectionType": "ALL"
                        },
                        "ProvisionedThroughput": {
                            "ReadCapacityUnits": 1,
                            "WriteCapacityUnits": 1
                        }
                    }
                }
            ],
        )
        print("Table updated successfully.")
    except Exception as e:
        print("Could not update table. Error:")
        print(e)
    

    In diesem Skript rufen wir eine update_table()-Methode auf unserem DynamoDB-Client auf. In der Methode übergeben wir Details über den Sekundärindex, den wir erstellen wollen, einschließlich des Schlüsselschemas für den Index, des bereitgestellten Durchsatzes und der in den Index zu projizierenden Attribute.

    Führen Sie das Skript aus, indem Sie den folgenden Befehl in Ihrem Terminal eingeben.

    python scripts/add_inverted_index.py

    Ihr Terminal zeigt die Ausgabe an, dass Ihr Index erfolgreich erstellt wurde.

    Table updated successfully.

    Im nächsten Schritt verwenden wir unseren invertierten Index, um alle Spielentitäten für einen bestimmten Benutzer abzurufen.

  • Schritt 8: Spiele für einen Benutzer abrufen

    Nun, da wir unseren invertierten Index erstellt haben, verwenden wir ihn, um die von einem Benutzer gespielten Spielentitäten abzurufen. Um dies zu handhaben, müssen wir den invertierten Index bei dem Benutzer abfragen, dessen Spielentitäten wir sehen wollen.

    In dem Code, den Sie heruntergeladen haben, befindet sich im Verzeichnis application/ ein find_games_for_user.py-Skript. Der Inhalt der Datei lautet wie folgt:

    import boto3
    
    from entities import UserGameMapping
    
    dynamodb = boto3.client('dynamodb')
    
    USERNAME = "carrpatrick"
    
    
    def find_games_for_user(username):
        try:
            resp = dynamodb.query(
                TableName='battle-royale',
                IndexName='InvertedIndex',
                KeyConditionExpression="SK = :sk",
                ExpressionAttributeValues={
                    ":sk": { "S": "USER#{}".format(username) }
                },
                ScanIndexForward=True
            )
        except Exception as e:
            print('Index is still backfilling. Please try again in a moment.')
            return None
    
        return [UserGameMapping(item) for item in resp['Items']]
    
    games = find_games_for_user(USERNAME)
    
    if games:
        print("Games played by {}:".format(USERNAME))
        for game in games:
            print(game)
    

    In diesem Skript haben wir eine Funktion namens find_games_for_user(), die einer Funktion ähnlich ist, die Sie in Ihrem Spiel haben würden. Diese Funktion nimmt einen Benutzernamen und gibt alle Spiele zurück, die von dem angegebenen Benutzer gespielt wurden.

    Führen Sie dieses Skript mit dem folgenden Befehl in Ihrem Terminal aus:

    python application/find_games_for_user.py

    Das Skript sollte alle Spiele, die der Benutzer carrpatrick spielt, drucken.

    Games played by carrpatrick:
    UserGameMapping<25cec5bf-e498-483e-9a00-a5f93b9ea7c7 -- carrpatrick -- SILVER>
    UserGameMapping<c6f38a6a-d1c5-4bdf-8468-24692ccc4646 -- carrpatrick>
    UserGameMapping<c9c3917e-30f3-4ba4-82c4-2e9a0e4d1cfd -- carrpatrick>
    

    In diesem Modul haben wir der Tabelle einen Sekundärindex hinzugefügt. Damit wurden zwei zusätzliche Zugangsmuster erfüllt:

    • Offene Spiele nach Karte finden (Lesevorgang)
    • Offene Spiele finden (Lesevorgang)

    Um dies zu erreichen, verwendeten wir einen spärlichen Index, der nur die Spiele enthielt, die noch für zusätzliche Spieler offen waren. Wir haben dann sowohl die Abfrage- als auch die Scan-APIs für den Index verwendet, um offene Spiele zu finden.

    Außerdem sahen wir, wie zwei fortgeschrittene Schreibvorgänge in der Anwendung erfüllt werden können. Zunächst verwendeten wir DynamoDB-Transaktionen, wenn ein Benutzer einem Spiel beitrat. Bei Transaktionen haben wir einen komplexen bedingten Schreibvorgang über mehrere Entitäten hinweg in einer einzigen Anfrage abgewickelt.

    Zweitens haben wir die Funktion für den Ersteller eines Spiels implementiert, um ein Spiel zu starten, wenn es fertig ist. Bei diesem Zugriffsmuster hatten wir einen Aktualisierungsvorgang, bei dem der Wert von drei Attributen überprüft und zwei Attribute aktualisiert werden mussten. Sie können diese komplexe Logik in einer einzigen Anfrage durch die Macht von Bedingungsausdrücken und Aktualisierungsausdrücken ausdrücken.

    Drittens haben wir das endgültige Zugriffsmuster erfüllt, indem wir alle von einem Benutzer gespielten Spielentitäten abgerufen haben. Um dieses Zugriffsmuster zu handhaben, haben wir einen Sekundärindex unter Verwendung des invertierten Indexmusters erstellt, um die Abfrage auf der anderen Seite der Viele-zu-Viele-Beziehung zwischen Benutzer- und Spielentitäten zu ermöglichen.

    Im nächsten Modul bereinigen wir die von uns geschaffenen Ressourcen.