Dans le module précédent, nous avons défini les modèles d’accès à l’application. Dans ce module, nous concevons le clé primaire pour la table DynamoDB et activons les modèles d’accès principaux.

Durée du module : 20 minutes


Lors de la conception de la clé primaire pour une table DynamoDB, ayez à l’esprit les bonnes pratiques suivantes :

  • Commencez par les différentes entités de votre table. Si vous stockez de nombreux types différents de données dans une table unique - tels que les employés, services, clients et commandes - assurez-vous que votre clé primaire dispose d’un moyen pour identifier distinctement chaque entité et permettre des actions essentielles sur les éléments individuels.
  • Employez des préfixes pour distinguer les types d’entité les uns des autres. L’emploi de préfixes pour distinguer les types d’entité les uns des autres peut éviter les collisions et faciliter les requêtes. Par exemple, si des clients et des employés sont réunis dans la même table, la clé primaire pour un client peut être CUSTOMER#<CUSTOMERID>, et pour un employé EMPLOYEE#<EMPLOYEEID>.
  • Concentrez-vous en premier sur les actions avec un seul élément, puis ajoutez ensuite des actions à éléments multiples quand cela est possible. Pour une clé primaire, il est essentiel que vous puissiez répondre aux options de lecture et d’écriture sur un seul élément à l’aide des API à élément unique : GetItem, PutItem, UpdateItem, etDeleteItem. Vous devez également pouvoir satisfaire à vos modèles de lecture à éléments multiples avec la clé primaire en utilisant Query(Requête). Sinon, vous pouvez ajouter un index secondaire pour les cas d’utilisation de Query (Requête).

Avec à l’esprit ces bonnes pratiques, concevons la clé primaire pour la table de l’application de jeu et réalisons quelques actions basiques.


  • Étape 1. Conception de la clef primaire

    Examinons d’abord les différentes entités, comme évoqué dans l’introduction. Le jeu comprend les entités ci-dessous :

    • User (Utilisateur)
    • Game(Jeu)
    • UserGameMapping(MappageJeuUtilisateur)

    Un UserGameMapping (MappageJeuUtilisateur) est un enregistrement qui indique qu’un utilisateur a rejoint une partie. Il existe des relations plusieurs à plusieurs entre User (Utilisateur) etGame (Jeu).

    Le fait d’avoir un mappage plusieurs à plusieurs est généralement une indication que vous souhaitez afin de satisfaire deux modèles de requête, et ce jeu ne fait pas exception. Vous avez un modèle d’accès qui a besoin de trouver tous les utilisateurs qui ont rejoint une partie, et un autre toutes les parties auxquelles un utilisateur a participé.

    Si votre modèle de données possède de nombreuses entités ayant des relations entre elle, vous utilisez généralement une clé primaire composite avec à la fois des valeurs HASH etRANGE. La clé primaire composite nous donne la capacité Query (Requête) sur la clé HASH pour satisfaire un des modèles de requête dont nous avons besoin. Dans la documentation DynamoDB, la clé de partition est dénommée HASH et la clé de tri RANGE, et dans ce guide nous utilisons indifféremment la terminologie API, en particulier à propos de code ou du format de protocole par fil JSON de DynamoDB.

    Les deux autres entités de données - User (Utilisateur) etGame (Jeu) - n’ont pas de propriété naturelle pour la valeur RANGE car les modèles d’accès sur un User ou Gamesont une recherche clé-valeur. Comme une valeur RANGE est nécessaire, nous pouvons fournir une valeur de remplissage pour la clé RANGE.

    Avec cela à l’esprit,utilisons le modèle suivant pour les valeurs HASH et RANGE pour chaque type d’entité.

    Entité HASH RANGE
    User (Utilisateur) USER#<USERNAME> #METADATA#<USERNAME>
    Game (Jeu) GAME#<GAME_ID> #METADATA#<GAME_ID>
    UserGameMapping (MappageJeuUtilisateur) GAME#<GAME_ID> USER#<USERNAME>

    Examinons la table précédente.

    Pour l’entité User (Utilisateur), la valeur HASH est USER#<USERNAME>. Notez que nous employons un préfixe pour identifier l’entité et éviter toute collision possible entre les types d’entité.

    Pour la valeur RANGE sur l’entité User (Utilisateur), nous employons un préfixe statique #METADATA# suivi par la valeurUSERNAME. Pour la valeur RANGE, il est essentiel d’avoir une valeur connue, tel le USERNAME. Cela permet des actions à élément unique telle que GetItem, PutItemet DeleteItem.

    Cependant, nous souhaitons également une valeur RANGE avec différentes valeurs sur les entités User (Utilisateur) pour activer un partitionnement égal si nous utilisons cette colonne comme une clé HASH pour un index. Pour cette raison, nous apposons le USERNAME.

    L’entité Game (Jeu) possède une conception de clé primaire similaire à celle de l’entité User (Utilisateur). Elle emploie un préfixe différent (GAME#) et un GAME_ID à la place de USERNAME, mais les principes sont identiques.

    Enfin, le UserGameMapping(MappageJeuUtilisateur) utilise la même clé HASH que l’entité Game (Jeu). Cela nous permet non seulement d’aller récupérer les métadonnées pour un Game mais aussi tous les utilisateurs dans jeu Game avec une requête unique. Nous utilisons ensuite l’entité User (Utilisateur) pour la clé RANGE sur le UserGameMapping (MappageJeuUtilisateur) pour identifier les joueurs qui ont rejoint une partie spécifique.

    Dans l’étape suivante, nous créons une table avec cette conception de clé primaire. 

  • Étape 2 : Création d’une table

    Après avoir créé la clé primaire, créons la table.

    Le code que vous avez téléchargé à l’étape 3 du Module 1 comprend dans le répertoire des scripts un script Python dénommé create_table.py. Le contenu du script Python est décrit ci-après.

    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)
    

    Le script précédent utilisait l’opération CreateTable à l’aide de Boto 3, le kit SDK AWS pour Python. L’opération déclare deux définitions d’attribut, qui sont des attributs qui doivent être saisis pour être utilisés dans la clé primaire. Même si DynamoDB est sans schéma, vous devez déclarer les noms et les types d’attribut que vous utilisez pour les clés primaires. Ces attributs doivent être inclus dans chaque élément qui est écrit dans la table et, par conséquent, doivent être spécifiés lors de la création de la table.

    Comme vous stockez des entités différentes dans une table unique, les noms d’attribut de la clé primaire, tel UserId, ne peuvent être employés. L’attribut désigne quelque chose de différent, basé sur le type d’entité qui est stocké. Par exemple, la clé primaire pour un utilisateur doit être son USERNAME, et celle pour une partie son GAMEID. Par conséquent, nous utilisons des noms génériques pour les attributs, tels que PK (pour clé de partition) etSK (pour clé de tri).

    Après la configuration des attributs dans le schéma de clé, nous spécifions le débit de pointe alloué pour la table. DynamoDB possède deux modes de capacité : allouée et à la demande. Dans le mode de capacité allouée, vous précisez exactement le volume du débit de lecture et d’écriture de pointe souhaité. Vous payez pour cette capacité, que vous l’utilisiez ou non.

    Dans DynamoDB, avec le mode de capacité à la demande, vous payez par requête. Le coût par requête est légèrement plus élevé que si vous utilisiez le mode de débit de pointe en totalité, mais vous ne perdez pas de temps à planifier les capacités, ou à craindre d’en manquer. Le mode à la demande est idéal pour les charges de travail avec des pics ou imprévisibles. Nous utilisons le mode de capacité allouée dans cet atelier car il convient parfaitement à l’offre gratuite DynamoDB.

    Pour créer la table, exécutez le script Python avec la commande suivante.

    python scripts/create_table.py

    Le script doit afficher en retour le message : « Table created successfully » (Table créée avec succès).

    Dans l’étape suivante, nous effectuons un chargement massif de données dans la table. 

  • Étape 3 : Chargement massif de données dans la table

    Dans cette étape, nous effectuons un chargement massif de données dans la base de données DynamoDB que nous avons créée à l’étape précédente. Cela signifie que pour mener les étapes suivantes, nous disposerons d’un échantillon de données .

    Dans le répertoire scripts existe un fichier dénomméitems.json. Ce fichier contient 835 exemples d’élément qui ont été aléatoirement générés pour cet atelier. Ces éléments comprennent les entités User (Utilisateur), Game (Jeu) etUserGameMapping (MappageJeuUtilisateur). Ouvrez le fichier pour voir quelques exemples d’élément.

    Le répertoire scripts contient également un fichier dénommé bulk_load_table.py qui lit les éléments dans le fichier items.json et les écrit de manière massive dans la table DynamoDB. Le contenu du fichier est décrit ci-après.

    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)
    

    Dans ce script, à la place du client bas-niveau dans Boto 3, nous utilisons un Resource object (Objet de ressource) de haut niveau. Les objets deressource fournissent une interface plus facile à utiliser avec les API AWS. L’objet deressource est utile dans cette situation car il regroupe nos requêtes. L’opération BatchWriteItem accepte jusqu’à 25 éléments dans une seule requête. L’objet de ressource gère ce regroupement pour nous, et par conséquent nous n’avons pas à partager nos données en requêtes de 25 éléments ou moins.

    Exécutez le script bulk_load_table.py et chargez votre table avec les données à l’aide des commandes suivantes dans le terminal.

    python scripts/bulk_load_table.py

    Vous pouvez vérifier que l’ensemble de vos données ont été chargées dans la table en exécutant un Scan pour obtenir le compte.

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

    Cela devrait afficher les résultats suivants.

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

    Vous devriez voir un compte de 835, ce qui signifie que tous vos éléments ont bien été chargés.

    Dans l’étape suivante, nous montrons comment récupérer plusieurs types d’entité avec une seule requête, et ainsi pouvoir réduire le nombre total de requêtes sur le réseau et améliorer les performances de l’application.

  • Étape 4 : Récupération de plusieurs types d’entité avec une seule requête

    Comme nous l’avons vu précédemment, vous pouvez optimiser les tables DynamoDB pour le nombre de requêtes qu’il reçoit. Nous avons aussi indiqué que DynamoDB ne dispose pas des jointures que possède une base de données relationnelle. À la place, vous concevez votre table afin qu’elle se comporte de la même façon que si elle avait des jointures pour vos requêtes.

    Dans cette étape, nous récupérons plusieurs types d’entité avec une seule requête. Dans le jeu, nous pouvons vouloir récupérer des informations sur une partie. Ces informations comprennent des détails sur la partie elle-même, l’heure de début, l’heure de fin, son initiateur, et sur les utilisateurs qui y ont participé.

    Cette requête recouvre deux types d’entité : l’entité Game (Jeu) et l’entité UserGameMapping (MappageJeuUtilisateur). Cependant, cela ne signifie pas que nous devons faire plusieurs requêtes.

    Le code que vous avez téléchargé comporte un script fetch_game_and_players.py dans le répertoire de l’application. Ce script montre comment vous pouvez structure votre code pour récupérer à la fois les entités Game (Jeu) et UserGameMapping (MappageJeuUtilisateur) pour la partie avec une seule requête.

    Le code suivant compose le 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)
    

    Au début de ce script, nous importons la bibliothèque Boto 3 et quelques catégories simples pour représenter les objets dans notre code d’application. Les définitions pour ces entités peuvent consultées dans le fichier application/entities.py.

    L’action initiée par le script a lieu dans la fonction fetch_game_and_users qui est définie dans le module. Cela est similaire à une fonction que vous définiriez dans votre application pour tout point de terminaison qui a besoin de ces données.

    La fonction fetch_game_and_users fait plusieurs choses. En premier, elle envoie une requête Query à DynamoDB. CetteQuery (Requête) utilise une PK (clé de partition) de GAME#<GameId>. Ensuite, elle interroge toute les entités pour lesquelles la clé de tri est entre#METADATA#<GameId> et USER$. Cela permet d’extraire l’entité Game (Jeu), dont la clé de tri est #METADATA#<GameId>, et toutes les entités UserGameMappings(MappageJeuUtilisateur) dont les clés commencent par USER#. Les clés de tri de type chaîne sont triées par codes de caractères ASCII. Le symbole dollar ($) vient directement après le signe de la livre (#) dans les caractèresASCII, ce qui garantit que nous obtiendrons tous les mappages de l’entité UserGameMapping (MappageJeuUtilisateur).

    Lorsque la réponse arrive, nous assemblons nos entités de données en objets reconnus par notre application. Nous savons que la première entité retournée est l’entité Game (Jeu) ; nous créons donc un objet Game à partir de l’entité. Pour les entités restantes, nous créons un objetUserGameMapping (MappageJeuUtilisateur) pour chaque entité, puis attachons l’ensemble des utilisateurs à l’objet Game(Jeu).

    La fin du script montre l’emploi de la fonction et imprime les objets qui en résultent. Vous pouvez exécuter le script dans votre terminal avec la commande suivante.

    python application/fetch_game_and_players.py

    Le script doit imprimer l’objet Game (Jeu) et tous les objets UserGameMapping (MappageJeuUtilisateur) vers la 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>
    

    Ce script montre comment vous pouvez organiser votre table et écrire vous requêtes pour récupérer plusieurs types d’entité avec une seule requête DynamoDB. Dans une base de données relationnelles, vous utilisez des jointures pour récupérer plusieurs types d’entité avec une seule requête. Avec DynamoDB, vous organisez spécifiquement vos données de telle façon que les entités auxquelles vous devez accéder ensemble soient disposées l’une près de l’autre dans une table unique. Cette méthode remplace les jointures nécessaires dans une base de données relationnelle et maintient les performances de votre application au fur et à mesure de sa croissance.


    Dans ce module nous avons conçu une clé primaire et créé une table. Puis nous avons chargé massivement des données dans une table et vu comment élaborer une requête unique pour récupérer plusieurs types d’entité.

    Avec cette conception de clé primaire, nous pouvons satisfaire aux modèles d’accès suivants :

    • Créer un profil utilisateur (Écriture)
    • Actualiser un profil utilisateur (Écriture)
    • Obtenir un profil utilisateur (Lecture)
    • Créer un jeu (Écriture)
    • Afficher un jeu (Lecture)
    • Rejoindre un jeu pour un utilisateur (Écriture)
    • Démarrer une partie(Écriture)
    • Actualiser un jeu pour un utilisateur (Écriture)
    • Actualiser un jeu (Écriture)

    Dans le prochain module, nous ajoutons un index secondaire et abordons la technique des index partiellement alloués. Les index secondaires vous permette de prendre en charge des modèles d’accès supplémentaires dans votre table DynamoDB.