Nel modulo precedente abbiamo definito i modelli di accesso all’applicazione mobile. In questo modulo, progetteremo la chiave principale per la tabella DynamoDB e abiliteremo i modelli di accesso principali.

Tempo necessario per completare il modulo: 40 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. Se la tua tabella contiene sia i clienti che i dipendenti, la chiave principale per un cliente potrebbe essere CUSTOMER#<CUSTOMERID>, mentre la chiave principale per un dipendente potrebbe 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. Sarebbe inoltre un’ottima cosa se riuscissi anche a 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 ed eseguiamo alcune azioni di base.


  • Fase 1. Progettazione della chiave principale

    Prendiamo in considerazione le diverse entità, come suggerito nell'introduzione qui sopra. Nell’applicazione mobile sono presenti le seguenti entità:

    • Utenti
    • Foto
    • Reazioni
    • Amicizia

    Queste entità mostrano tre diversi tipi di relazioni tra dati.

    Per prima cosa, ogni utente sulla tua applicazione avrà un unico profilo utente rappresentato da un’entità Utente nella tua tabella.

    Quindi, un utente disporrà di più foto rappresentate nella tua applicazione e a una foto corrisponderanno più reazioni. Entrambe queste relazioni sono da uno a molti.

    Infine, l’entità Amicizia è una rappresentazione di una relazione da molti a molti. L’entità Amicizia rappresenta quando un utente segue un altro utente nella tua applicazione. È una relazione da molti a molti perché un utente può seguire più utenti ma può a sua volta avere più follower.

    Disporre di una mappatura molti-a-molti indica generalmente che desideri soddisfare i due modelli di Query e che la nostra applicazione non fa eccezione. Nell’entità Amicizia, disponiamo di un modello di accesso che deve trovare tutti gli utenti che seguono un determinato utente oltre a un modello di accesso per trovare tutti gli utenti che un determinato utente segue.

    Per ottenere questo risultato, utilizzeremo una chiave principale composta con un valore che è sia HASH che 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 specifica dell’API 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 di collegamento JSON di DynamoDB.

    L’entità uno a uno Utente non dispone di una proprietà naturale per il valore RANGE. Poiché è un collegamento da uno a uno, i modelli di accesso saranno una ricerca base con valore chiave. E poiché la progettazione della tua tabella richiede una proprietà RANGE, puoi fornire un valore di riempimento 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>

    Foto

    USER#<USERNAME>

    PHOTO#<USERNAME>#<TIMESTAMP>

    Reazione

    REACTION#<USERNAME>#<TYPE>

    PHOTO#<USERNAME>#<TIMESTAMP>

    Amicizia

    USER#<USERNAME>

    #FRIEND#<FRIEND_USERNAME>

    Esaminiamo passo passo la tabella qui sopra.

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

    Per il valoreRANGE 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, ti serve un valore RANGE con valori diversi tra le varie entità Utente per consentire una partizione uniforme in caso di utilizzo di questa colonna come chiave HASH per un indice. Per tale motivo, si accoda username alla chiave RANGE.

    In secondo luogo, l’entità Foto è un’entità secondaria di una specifica entità Utente. Il modulo di accesso principale per le foto consiste nel recupero di foto per un utente ordinate per data. Quando occorre ordinare qualcosa per una specifica proprietà, dovrai includere tale proprietà nella tua chiave RANGE per consentire l’ordinamento. Per l’entità Foto, utilizza la stessa chiave HASH dell’entità Utente, che ti consentirà di recuperare sia un profilo utente sia le foto dell’utente con un’unica richiesta. Per la chiave RANGE, utilizza PHOTO#<USERNAME>#<TIMESTAMP> per identificare una foto in modo univoco nella tua tabella.

    In terzo luogo, l’entità Reazione è un’entità secondaria di una specifica entità Foto. Esiste una relazione da uno a molti all’entità Foto che utilizzerà una logica simile a quella dell’entità Foto. Nel prossimo modulo, scoprirai come recuperare una foto e tutte le relative reazioni con un’unica query utilizzando un indice secondario. Per ora, tieni a mente che la chiave RANGE per un’entità Reazione ha lo stesso modello della chiave RANGE di un’entità Foto. Per la chiave HASH, utilizziamo lo username dell’utente che sta creando la reazione nonché il tipo di reazione applicata. Accodare il tipo di reazione consente a un utente di aggiungere più tipi di reazioni a un’unica foto.

    Infine, l’entità Amicizia utilizza la stessa chiave HASH come entità Utente. Ciò ti consentirà di recuperare sia i metadati di un utente sia tutti i follower dell’utente con un’unica query. La chiave RANGE per un’entità Amicizia è #FRIEND#<FRIEND_USERNAME>. Nella Fase 4 di seguito, scoprirai perché occorre anteporre alla chiave RANGE dell’entità Amicizia un “#”.

    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 i seguenti:

    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)

    Lo script precedente utilizza l'operazione CreateTable tramite Boto 3, l'SDK AWS per Python. L'operazione dichiara due definizioni di attributi che sono attributi con tipo per essere utilizzati nella 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, per la chiave principale non è possibile utilizzare nomi degli attributi come UserId. L'attributo significa una cosa diversa in base al tipo di entità che si sta archiviando. Ad esempio, la chiave principale per un utente potrebbe essere il suo USERNAME, mentre la chiave principale per una reazione potrebbe essere il relativo TYPE. 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 blocco alcuni dati nel DynamoDB creato nella fase precedente. Ciò significa che nelle fasi successive, disporremo di dati di esempio da utilizzare.

    Nella directory scripts/, è presente un file denominato items.json. Questo file contiene 967 voci di esempio generate in maniera casuale per questo progetto. Queste voci includono le entità Utente, Foto, Amicizia e Reazione. Se desideri visualizzare alcuni dati 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 blocco nella tabella DynamoDB. Il file contiene quanto segue:

    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)

    Anziché 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 API BatchWriteItem accetta fino a 25 voci in un'unica richiesta. L'oggetto Resource gestisce questa operazione di creazione 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 stati caricati correttamente nella tabella, eseguendo l'operazione Scan ed effettuando la somma.

    Esegui il comando seguente per utilizzare l’interfaccia a riga di comando di AWS per ottenere la somma:

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

    Questa procedura dovrebbe mostrare i risultati seguenti.

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

    Dovresti visualizzare un Conteggio di 967, 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. Nella nostra applicazione, potrebbe essere utile recuperare informazioni su un utente. Sono incluse tutte le informazioni nel profilo dell’utente sull’entità Utente e tutte le foto che sono state caricate da un utente.

    Questa richiesta abbraccia due tipi di entità: l’entità Utente e l’entità Foto. Tuttavia, ciò non significa che dobbiamo effettuare più richieste.

    Nel codice che hai scaricato, nella directory application/ è presente un file denominato fetch_user_and_photos.py. Lo script mostra come poter strutturare il codice per recuperare sia un'entità Utente che le entità Foto che sono state caricate dall’utente con un'unica richiesta.

    Il codice seguente compone lo script fetch_user_and_photos.py.

    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)

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

    Il lavoro effettivo avviene nella funzione fetch_user_and_photos 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.

    In questa funzione, per prima cosa avanzi una richiesta Query a DynamoDB. La Query specifica una chiave HASH di USER#<Username> per isolare le voci restituite a uno specifico utente.

    Quindi, la Query specifica un’espressione della condizione della chiave RANGE compresa tra #METADATA#<Username> e PHOTO$. Questa Query restituisce un’entità Utente, poiché la sua chiave di ordinamento è #METADATA#<Username>, nonché tutte le entità Foto per questo utente, le cui chiavi di ordinamento iniziano con PHOTO#. 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à Foto.

    Una volta ricevuta una risposta, assembliamo le voci in oggetti noti alla nostra applicazione. Sappiamo che la prima voce restituita sarà la nostra entità Utente, quindi creiamo un oggetto Utente a partire dalla voce. Per le altre voci, creiamo un oggetto Foto per ciascuna e quindi alleghiamo l'array di utenti all'oggetto Utente.

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

    python application/fetch_user_and_photos.py

    Dovrebbe stampare l’oggetto Foto e tutti gli oggetti Foto nella console:

    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>

    Questo script illustra come puoi modellizzare 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 blocco 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, siamo in grado di soddisfare i modelli di accesso seguenti:

    • Crea profilo utente (Scrivi)
    • Aggiorna profilo utente (Scrivi)
    • Ottieni profilo utente (Leggi)
    • Carica foto (Scrivi)
    • Visualizza foto dell'utente (Leggi)
    • Visualizza amici dell’utente (Leggi)

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