Im vorherigen Modul haben wir die Zugriffsmuster der mobile Anwendung 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: 40 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 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. Wenn Sie mit Hilfe von Abfrage auch Lesemuster mit mehreren Elementen mit dem Primärschlüssel befriedigen können, ist das großartig. Wenn nicht, können Sie immer 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 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. In unserer mobilen Anwendung haben wir die folgenden Entitäten:

    • Benutzer
    • Fotos
    • Reaktionen
    • Freundschaft

    Diese Entitäten zeigen drei verschiedene Arten von Datenbeziehungen.

    Zunächst wird jeder Benutzer in Ihrer Anwendung über ein einziges Benutzerprofil verfügen, das durch eine Benutzerentität in Ihrer Tabelle repräsentiert wird.

    Als nächstes wird ein Benutzer mehrere Fotos in Ihrer Anwendung darstellen lassen, und ein Foto wird mehrere Reaktionen hervorrufen. Dies sind beides Eins-zu-viele-Beziehungen.

    Schließlich ist die Freundschaftsentität eine Darstellung einer Viele-zu-viele-Beziehung. Die Freundschaftsentität stellt dar, wenn ein Benutzer einem anderen Benutzer in Ihrer Anwendung folgt. Es handelt sich um eine Viele-zu-viele-Beziehung, da ein Benutzer mehreren anderen Benutzern folgen kann, und ein Benutzer kann mehrere Follower haben.

    Ein Viele-zu-viele-Zuordnung ist in der Regel ein Hinweis darauf, dass Sie zwei Abfragemuster erfüllen wollen, und unsere Anwendung bildet da keine Ausnahme. In der Entität Freundschaft haben wir ein Zugriffsmuster, das alle Benutzer finden muss, die einem bestimmten Benutzer folgen, sowie ein Zugriffsmuster, das alle Benutzer finden muss, denen ein bestimmter Benutzer folgt.

    Aus diesem Grund werden wir einen zusammengesetzten Primärschlüssel mit einem HASH- und einem RANGE-Wert verwenden. 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-API-Spezifikation wird der Partitionsschlüssel als HASH und der Sortierschlüssel als RANGE bezeichnet, und in diesem Leitfaden werden wir die API-Terminologie austauschbar verwenden, insbesondere wenn wir den Code oder das DynamoDB-JSON-Drahtprotokollformat diskutieren.

    Beachten Sie, dass die Eins-zu-Eins-Entität -- Benutzer -- keine natürliche Eigenschaft für den Wert RANGE hat. Da es sich um eine Eins-zu-eins-Zuordnung handelt, werden die Zugriffsmuster eine grundlegendes Schlüssel-Werte-Suche sein. Da Ihr Tabellendesign eine RANGE-Eigenschaft erfordert, können Sie einen Füllwert für den RANGE-Schlüssel angeben.

    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>

    Foto

    USER#<USERNAME>

    PHOTO#<USERNAME>#<TIMESTAMP>

    Reaktion

    REACTION#<USERNAME>#<TYPE>

    PHOTO#<USERNAME>#<TIMESTAMP>

    Freundschaft

    USER#<USERNAME>

    #FRIEND#<FRIEND_USERNAME>

    Gehen wir die vorangegangene Tabelle durch.

    Zunächst wird der HASH-Wert für die Benutzerentität USER#<USERNAME> sein. Beachten Sie, dass Sie 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 hängen Sie den Benutzernamen an den RANGE-Schlüssel an.

    Zweitens ist die Fotoentität eine untergeordnete Entität einer bestimmten Benutzerentität. Das Hauptzugriffsmuster für Fotos besteht darin, Fotos für einen Benutzer nach Datum geordnet abzurufen. Wann immer Sie etwas nach einer bestimmten Eigenschaft sortiert benötigen, müssen Sie diese Eigenschaft in Ihren RANGE-Schlüssel aufnehmen, um eine Sortierung zu ermöglichen. Verwenden Sie für die Fotoentität denselben HASH-Schlüssel wie für die Benutzerentität, wodurch Sie sowohl ein Benutzerprofil als auch die Fotos des Benutzers in einer einzigen Anfrage abrufen können. Verwenden Sie für den RANGE-Schlüssel PHOTO#<USERNAME>#<TIMESTAMP>, um ein Foto in Ihrer Tabelle eindeutig zu identifizieren.

    Drittens ist die Entität „Reaktion“ eine untergeordnete Entität einer bestimmten Entität „Foto“. Es besteht eine Eins-zu-viele-Beziehung zur Entität „Foto“ und es wird daher eine ähnliche Argumentation wie bei der Entität „Foto“ verwendet. Im nächsten Modul sehen Sie, wie Sie ein Foto und alle seine Reaktionen in einer einzigen Abfrage mit Hilfe eines Sekundärindexes abrufen können. Beachten Sie zunächst, dass der Schlüssel RANGE für eine Entität „Reaktion“ dasselbe Muster aufweist wie der Schlüssel RANGE für eine Entität „Foto“. Für den HASH-Schlüssel verwenden wir den Benutzernamen des Benutzers, der die Reaktion erstellt, sowie die Art der angewandten Reaktion. Das Anhängen des Reaktionstyps ermöglicht es einem Benutzer, mehrere Reaktionstypen zu einem einzigen Foto hinzuzufügen.

    Schließlich verwendet die Entität „Freundschaft“ denselben HASH-Schlüssel wie die Entität „Benutzer“. Auf diese Weise können Sie sowohl die Metadaten für einen Benutzer als auch alle Follower des Benutzers in einer einzigen Abfrage abrufen. Der RANGE-Schlüssel für eine Entität „Freundschaft“ ist #FRIEND#<FRIEND_USERNAME>. In Schritt 4 unten erfahren Sie, warum dem Schlüssel RANGE der Entität „Friendship“ ein # vorangestellt werden sollte.

    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 ist wie folgt:

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.create_table(
            TableName='quick-photos',
            AttributeDefinitions=[
                {
                    "AttributeName": "PK",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "SK",
                    "AttributeType": "S"
                }
            ],
            KeySchema=[
                {
                    "AttributeName": "PK",
                    "KeyType": "HASH"
                },
                {
                    "AttributeName": "SK",
                    "KeyType": "RANGE"
                }
            ],
            ProvisionedThroughput={
                "ReadCapacityUnits": 5,
                "WriteCapacityUnits": 5
            }
        )
        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 Sie verschiedene Entitäten in einer einzigen Tabelle speichern, kann Ihr Primärschlüssel keine 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 USERNAME sein, und der Primärschlüssel für eine Reaktion könnte sein TYPE sein. Dementsprechend verwenden wir generische Namen für die Attribute – 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

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

    Im Verzeichnis scripts/ gibt es eine Datei namens items.json. Diese Datei enthält 967 Beispielelemente, die nach dem Zufallsprinzip für unser Projekt generiert wurden. Zu diesen Elementen gehören Benutzer- , Foto- , Freundschafts- und Reaktion-Entitäten. Öffnen Sie die Datei, wenn Sie einige der Beispieldaten 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 lautet wie folgt:

    import json
    
    import boto3
    
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('quick-photos')
    
    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 Ressourcenobjekt ist in dieser Situation nützlich, weil es unsere Anfragen bündelt. Der Vorgang BatchWriteItem API akzeptiert bis zu 25 Elemente in einer einzigen Anfrage. Das Ressource-Objekt wird diese Stapelung für uns handhaben, anstatt uns zu zwingen, unsere Daten in Anfragen von 25 Elementen oder weniger zu zerhacken.

    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 geladen wurden, indem Sie einen Scan-Vorgang durchführen und die Zählung zurücksenden.

    Führen Sie den folgenden Befehl aus, um die AWS-CLI zum Abrufen der Zählung zu verwenden:

    aws dynamodb scan \
     --table-name quick-photos \
     --select COUNT

    Dies sollte die folgenden Ergebnisse anzeigen.

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

    Sie sollten eine Anzahl von 967 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 sehen wir, wie mehrere Entitätstypen in einer einzigen Anfrage abgerufen werden können. In unserer Anwendung möchten wir möglicherweise Informationen über einen Benutzer abrufen. Dazu gehören alle Informationen im Profil des Benutzers über die Entität Benutzer sowie alle Fotos, die von einem Benutzer hochgeladen wurden.

    Diese Anfrage erstreckt sich auf zwei Entitätstypen – die Entität Benutzer und die Entität Foto. Das bedeutet jedoch nicht, dass wir mehrere Anfragen stellen müssen.

    In dem Code, den Sie heruntergeladen haben, gibt es im Verzeichnis application/ eine Datei namens fetch_user_and_photos.py. Dieses Skript zeigt, wie Sie Ihren Code strukturieren können, um sowohl eine Entität Benutzer als auch die Entität Foto abzurufen, die vom Benutzer in einer einzigen Anfrage hochgeladen wurden.

    Der folgende Code setzt das Skript fetch_user_and_photos.py zusammen.

    import boto3
    
    from entities import User, Photo
    
    dynamodb = boto3.client('dynamodb')
    
    USER = "jacksonjason"
    
    
    def fetch_user_and_photos(username):
        resp = dynamodb.query(
            TableName='quick-photos',
            KeyConditionExpression="PK = :pk AND SK BETWEEN :metadata AND :photos",
            ExpressionAttributeValues={
                ":pk": { "S": "USER#{}".format(username) },
                ":metadata": { "S": "#METADATA#{}".format(username) },
                ":photos": { "S": "PHOTO$" },
            },
            ScanIndexForward=True
        )
    
        user = User(resp['Items'][0])
        user.photos = [Photo(item) for item in resp['Items'][1:]]
    
        return user
    
    
    user = fetch_user_and_photos(USER)
    
    print(user)
    for photo in user.photos:
        print(photo)

    Zuerst 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, falls Sie interessiert sind.

    Die eigentliche Arbeit findet in der Funktion fetch_user_and_photos statt, 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.

    Bei dieser Funktion stellen Sie zunächst eine Abfrageanforderung an DynamoDB. Die Abfrage gibt einen HASH-Schlüssel von USER#<Username> an, um die zurückgegebenen Elemente für einen bestimmten Benutzer zu isolieren.

    Dann gibt die Abfrage einen RANGE-Schlüsselbedingungsausdruck an, der zwischen #METADATA#<Username> und PHOTO$ liegt. Diese Abfrage gibt eine Benutzerentität zurück, da ihr Sortierschlüssel #METADATA#<Username> lautet, sowie alle Foto-Entitäten für diesen Benutzer, deren Sortierschlüssel mit PHOTO# beginnt. Sortierschlüssel vom Typ String werden nach ASCII-Zeichencodes sortiert. Das Dollar-Zeichen ($) kommt direkt nach dem Pfund-Zeichen (#) in ASCII, sodass sichergestellt ist, dass wir alle Foto-Entitäten erhalten.

    Sobald wir eine Antwort erhalten, setzen wir unsere Elemente zu Objekten zusammen, die unserer Anwendung bekannt sind. Wir wissen, dass das erste zurückgegebene Element unsere Entität Benutzer sein wird, also erstellen wir ein Objekt Benutzer aus dem Element. Für die übrigen Objekte erstellen wir für jedes ein Foto-Objekt und hängen dann das Array der Benutzer an das Benutzer-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_user_and_photos.py

    Es sollte das Benutzer-Objekt und alle Foto-Objekte auf die Konsole drucken:

    User<jacksonjason -- John Perry>
    Photo<jacksonjason -- 2018-05-30T15:42:38>
    Photo<jacksonjason -- 2018-06-09T13:49:13>
    Photo<jacksonjason -- 2018-06-26T03:59:33>
    Photo<jacksonjason -- 2018-07-14T10:21:01>
    Photo<jacksonjason -- 2018-10-06T22:29:39>
    Photo<jacksonjason -- 2018-11-13T08:23:00>
    Photo<jacksonjason -- 2018-11-18T15:37:05>
    Photo<jacksonjason -- 2018-11-26T22:27:44>
    Photo<jacksonjason -- 2019-01-02T05:09:04>
    Photo<jacksonjason -- 2019-01-23T12:43:33>
    Photo<jacksonjason -- 2019-03-03T02:00:01>
    Photo<jacksonjason -- 2019-03-03T18:20:10>
    Photo<jacksonjason -- 2019-03-11T15:18:22>
    Photo<jacksonjason -- 2019-03-30T02:28:42>
    Photo<jacksonjason -- 2019-04-14T21:52:36>

    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 unserem derzeitigen Primärschlüsseldesign können wir die folgenden Zugriffsmuster erfüllen:

    • Benutzerprofil erstellen (Schreibvorgang)
    • Benutzerprofil aktualisieren (Schreibvorgang)
    • Benutzerprofil abrufen (Lesevorgang)
    • Foto hochladen (Schreibvorgang)
    • Fotos für Benutzer anzeigen (Lesevorgang)
    • Freunde eines Benutzers anzeigen (Lesevorgang)

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