Nel modulo precedente abbiamo definito i modelli di accesso al gioco. In questo modulo, progetteremo la chiave principale per la tabella DynamoDB e abiliteremo i modelli di accesso al core.

Tempo necessario per completare il modulo: 20 minuti


Durante la progettazione della chiave principale per la tabella DynamoDB, ricorda queste best practice:

  • Inizia con le varie entità presenti nella tabella. Se stai archiviando più tipi diversi di dati in un'unica tabella, come per esempio dipendenti, dipartimenti, clienti e ordini, assicurati che la chiave principale disponga di una modalità di identificazione distinta per ogni entità e abilita le azioni del core sui singoli elementi.
  • Utilizza i prefissi per distinguere i tipi di entità. L'uso di prefissi per distinguere i tipi di entità consente di prevenire i conflitti e ti supporta nell'esecuzione di query. Ad esempio, se la tua tabella contiene sia i clienti che i dipendenti, la chiave principale per un cliente deve essere CUSTOMER#<CUSTOMERID>, mentre la chiave principale per un dipendente deve essere EMPLOYEE#<EMPLOYEEID>.
  • Poni la tua attenzione prima di tutto sulle azioni a voce singola, quindi aggiungi le azioni a voci multiple, se possibile. Per quanto riguarda la chiave principale, è importante riuscire a soddisfare le opzioni di lettura e scrittura su una singola voce utilizzando API a voce singola: GetItem, PutItem, UpdateItem e DeleteItem. Inoltre, potresti soddisfare i modelli di lettura a voci multiple tramite una chiave principale utilizzando la funzionalità Query. In alternativa, puoi aggiungere un indice secondario per la gestione dei casi d'uso Query.

Tenendo presente queste best practice, iniziamo a progettare la chiave principale per la tabella del gioco ed eseguiamo alcune azioni di base.


  • Fase 1. Progettazione della chiave principale

    Prendiamo in considerazione le diverse entità, come suggerito nell'introduzione qui sopra. Nel gioco sono presenti le seguenti entità:

    • Utente
    • Gioco
    • UserGameMapping

    Per UserGameMapping si intende un record che riporta un utente che ha partecipato a un gioco. Esiste una relazione molti-a-molti tra Utente e Gioco.

    Disporre di una mappatura molti-a-molti indica generalmente che desideri soddisfare i due modelli di Query e che il gioco non fa eccezione. Abbiamo un modello di accesso che deve trovare tutti gli utenti che partecipano al gioco e un altro modello per trovare tutti i giochi a cui un utente ha partecipato.

    Se il modello di dati presenta più entità in relazione tra loro, normalmente userai una chiave principale composita comprendente sia i valori HASH che i valori RANGE. La chiave principale composita ci consente di utilizzare le funzionalità Query con la chiave HASH per soddisfare uno dei modelli di query che ci servono. Nella documentazione DynamoDB, la chiave di partizione prende il nome di HASH, mentre la chiave di ordinamento è la cosiddetta RANGE. In questa guida, useremo la terminologia API in modo intercambiabile, in particolare nel parlare del codice o del formato del protocollo di collegamento JSON di DynamoDB.

    Le altre due entità di dati (Utente e Gioco) non dispongono di proprietà naturali per il valore RANGE, dato che i modelli di accesso a Utente o Gioco sono una ricerca del valore chiave. Visto che il valore RANGE è obbligatorio, possiamo fornire un valore di filtro per la chiave RANGE.

    Tenendo presente questo concetto, iniziamo a utilizzare il modello seguente per i valori HASH e RANGE per tutti i tipi di entità.

    Entità HASH RANGE
    Utente USER#<USERNAME> #METADATA#<USERNAME>
    Gioco GAME#<GAME_ID> #METADATA#<GAME_ID>
    UserGameMapping GAME#<GAME_ID> USER#<USERNAME>

    Esaminiamo passo passo la tabella qui sopra.

    Per l'entità Utente il valore HASH equivale a USER#<USERNAME>. Nota che utilizziamo un prefisso per identificare l'entità ed evitare possibili collisioni tra tipi di entità.

    Per il valore RANGE sull'entità Utente, utilizziamo un prefisso statico di #METADATA# seguito dal valore USERNAME. Per il valore RANGE, è fondamentale disporre di un valore noto, come il valore USERNAME. Questo ci consente di effettuare azioni a voce singola, quali GetItem, PutItem e DeleteItem.

    Tuttavia, vogliamo anche un valore RANGE con diversi valori tra le varie entità Utente per consentire una partizione uniforme in caso di utilizzo di questa colonna come chiave HASH per un indice. Per questo motivo, abbiamo aggiunto il valore USERNAME.

    L'entità Gioco dispone di una progettazione della chiave principale analogia alla progettazione dell'entità Utente. Utilizza un prefisso diverso (GAME#) e GAME_ID al posto di USERNAME, ma i principi sono gli stessi.

    Infine, UserGameMapping usa la stessa chiave HASH dell'entità Gioco. Questo ci consente di recuperare non solo i metadati per un Gioco, ma anche tutti gli utenti presenti in un Gioco in un'unica query. Quindi, utilizziamo l'entità Utente per la chiave RANGE sul UserGameMapping per identificare quale utente ha partecipato a un gioco specifico.

    Nella fase successiva, creeremo una tabella con la progettazione della chiave principale. 

  • Fase 2: Creazione di una tabella

    Adesso che abbiamo progettato la chiave principale, possiamo creare una tabella.

    Il codice scaricato nella Fase 3 del Modulo 1 include uno script Python nella directory scripts/ denominato create_table.py. I contenuti dello script Python sono riportati di seguito.

    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)
    

    Lo script precedente utilizza l'operazione CreateTable tramite Boto 3, l'AWS SDK per Python. L'operazione dichiara due definizioni di attributi che sono attributi con tipo per essere utilizzati come chiave principale. Anche se DynamoDB è privo di schemi, devi dichiarare i nomi e i tipi di attributi utilizzati per le chiavi principali. Gli attributi devono essere inclusi in ogni voce scritta nella tabella e, pertanto, devono essere specificati in fase di creazione della tabella.

    Dato che archiviamo diverse entità in un'unica tabella, non possiamo utilizzare i nomi degli attributi della chiave principale, come UserId. L'attributo significa che è in corso l'archiviazione di qualcosa di diverso in base al tipo dell'entità. Ad esempio, la chiave principale per un utente potrebbe essere il suo USERNAME, mentre la chiave principale per un gioco potrebbe essere il relativo GAMEID. Di conseguenza, usiamo nomi generici per gli attributi, ad esempio PK (per la chiave di partizione) e SK (per la chiave di ordinamento).

    Dopo aver configurato gli attributi nello schema della chiave, specifichiamo il throughput assegnato alla tabella. DynamoDB dispone di due modalità di capacità: assegnata e on demand. Nella modalità di capacità assegnata, specifichi esattamente la mole di throughput in lettura e scrittura desiderata. Indipendentemente dall'utilizzo, ti sarà addebitato il costo per questa capacità.

    Nella modalità di capacità on demand di DynamoDB, puoi pagare per ogni richiesta. Il costo per richiesta è leggermente più alto rispetto all'uso di throughput interamente assegnati, ma non devi investire del tempo per la pianificazione della capacità o preoccupandoti di essere sottoposto a limitazioni. La modalità on demand funziona molto bene per lavori imprevedibili e che presentano picchi di carico. In questo corso usiamo la modalità di capacità assegnata perché si adatta al piano gratuito di DynamoDB.

    Per creare una tabella, esegui lo script Python con il comando seguente.

    python scripts/create_table.py

    Lo script deve restituire questo messaggio: "Tabella creata correttamente".

    Nel passaggio successivo, effettuiamo il caricamento bulk di alcuni dati di esempio nella tabella. 

  • Fase 3: Caricamento bulk dei dati nella tabella

    In questo passaggio, carichiamo in bulk alcuni dati nel DynamoDB creato nella fase precedente. Ciò significa che nelle fasi successive, disporremo di dati di esempio da utilizzare.

    Nella directory scripts/ troverai un file denominato items.json. Questo file contiene 835 voci di esempio generate in maniera casuale per questo corso. Queste voci includono le entità Utente, Gioco e UserGameMapping. Se desideri visualizzare alcune voci di esempio, apri il file.

    La directory scripts/ dispone anche di un file denominato bulk_load_table.py che legge le voci presenti nel file items.json e le scrive in bulk nella tabella DynamoDB. Il contenuto di questo file è riportato di seguito.

    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)
    

    Al posto di utilizzare il client di basso livello in Boto 3, in questo script usiamo un oggetto Resource di livello più elevato. Gli oggetti Resource forniscono una semplice interfaccia per l'utilizzo delle API di AWS. L'oggetto Resource è utile in questa situazione perché effettua il batch delle nostre richieste. L'operazione BatchWriteItem accetta fino a 25 voci in un'unica richiesta. L'oggetto Resource gestisce questo batch per nostro conto, evitando così di farci suddividere i dati in richieste inferiori o pari a 25 voci l'una.

    Esegui lo script bulk_load_table.py e carica la tabella con i dati eseguendo il comando seguente nel terminale.

    python scripts/bulk_load_table.py

    Puoi assicurarti che tutti i dati siano caricati correttamente nella tabella, eseguendo l'operazione Scan ed effettuando la somma.

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

    Questa procedura dovrebbe mostrare i risultati seguenti.

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

    Dovresti visualizzare un Conteggio di 835, che indica che tutte le voci sono state caricate correttamente.

    Nella fase successiva, ti mostreremo come ripristinare diversi tipi di entità in un'unica richiesta in grado di ridurre la mole complessiva di richieste della rete effettuate nell'applicazione e incrementare le prestazioni dell'applicazione.

  • Fase 4: Ripristino di diversi tipi di entità in un'unica richiesta

    Come illustrato nel modulo precedente, devi ottimizzare le tabelle DynamoDB per il numero di richieste ricevute. Abbiamo anche citato il fatto che DynamoDB non dispone dei join che ha un database relazionale. Al contrario, progetti la tabella per ricreare un comportamento simile a quello dei join nelle richieste.

    In questa fase, ripristiniamo i diversi tipi di entità in un'unica richiesta. In un gioco, potremmo desiderare di recuperare i dettagli su una sessione di gioco. Questi dettagli includono informazioni sul gioco stesso, ad esempio l'orario di avvio, l'orario di conclusione del gioco, chi l'ha posizionato in quel punto e dettagli sugli utenti che hanno preso parte al gioco.

    Questa richiesta si estende a due tipi di entità: l'entità Gioco e l'entità UserGameMapping. Tuttavia, ciò non significa che dobbiamo effettuare più richieste.

    Nel codice scaricato, troverai uno script fetch_game_and_players.py nella directory application/. Lo script mostra come poter strutturare il codice per recuperare sia l'entità Gioco che l'entità UserGameMapping per il gioco in un'unica richiesta.

    Il codice seguente compone lo script fetch_game_and_players.py.

    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)
    

    All'inizio dello script importiamo la libreria Boto 3 e alcune semplici classi per rappresentare gli oggetti nel codice dell'applicazione. Puoi visualizzare le definizioni per queste entità nel file application/entities.py.

    Il lavoro effettivo dello script si verifica nella funzione fetch_game_and_users definita nel modulo. Si tratta di una funzione simile a quella che definiresti nell'applicazione per essere utilizzata dagli endpoint che hanno bisogno di questi dati.

    La funzione fetch_game_and_users effettua alcune azioni. Innanzitutto, effettua una richiesta Query a DynamoDB. Questa Query usa un PK di GAME#<GameId>. Quindi, richiede tutte le entità in cui la chiave di ordinamento è compresa tra #METADATA#<GameId> e USER$. In questo modo copia l'entità Gioco , la cui chiave di ordinamento è #METADATA#<GameId>, e tutte le entità UserGameMappings, le cui chiavi iniziano con USER#. Le chiavi di ordinamento dei tipi di stringa sono ordinate in base ai codici dei caratteri ASCII. Il simbolo del dollaro ($) viene subito dopo quello della sterlina (#) secondo i codici ASCII; questo garantisce la mappatura integrale nell'entità UserGameMapping.

    Quando riceviamo una risposta, assembliamo le entità dei dati in oggetti noti dalla nostra applicazione. Sappiamo che la prima entità restituita è l'entità Gioco; creiamo quindi un oggetto Gioco dall'entità. Per le altre entità, creiamo un oggetto UserGameMapping per tutte le entità, quindi alleghiamo l'array degli utenti all'oggetto Gioco.

    La parte finale dello script mostra l'utilizzo della funzione e stampa degli oggetti. Puoi eseguire lo script nel terminale con il comando seguente.

    python application/fetch_game_and_players.py

    Lo script deve stampare l'oggetto Gioco e tutti gli oggetti UserGameMapping sulla console.

    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>
    

    Questo script illustra come puoi creare i modelli per la tabella e scrivere le query per restituire diversi tipi di entità in un'unica richiesta DynamoDB. In un database relazionale, usi i join per recuperare diversi tipi di entità da varie tabelle in un'unica richiesta. Con DynamoDB, crei modelli di dati appositamente per far sì che le entità a cui devi accedere in contemporanea si trovino una accanto all'altra in un'unica tabella. Questo approccio fa sì che i join non siano più necessari in un tradizionale database relazionale e garantisce elevate prestazioni alla tua applicazione in base alla scalabilità.


    In questo modulo, abbiamo progettato una chiave principale e creato una tabella. Quindi, abbiamo effettuato il caricamento in bulk dei dati nella tabella ed esaminato come effettuare le query per diversi tipi di entità in un'unica richiesta.

    Con la progettazione della chiave principale corrente, possiamo soddisfare i modelli di accesso seguenti:

    • Crea profilo utente (Scrivi)
    • Aggiorna profilo utente (Scrivi)
    • Ottieni un profilo utente (Leggi)
    • Crea gioco (Scrivi)
    • Visualizza gioco (Leggi)
    • Partecipa a un gioco per un utente (Scrivi)
    • Avvia gioco (Scrivi)
    • Aggiorna gioco per un utente (Scrivi)
    • Aggiorna gioco (Scrivi)

    Nel modulo successivo, aggiungeremo un indice secondario e scopriremo in cosa consiste la tecnica dell'indicizzazione sparsa. Gli indici secondari ti consentono di supportare ulteriori modelli di accesso nella tabella DynamoDB.