Krisna
Krisna

Reputation: 3423

Dynamodb-serverless one to many and many to many relationships

I am really new to Dynamodb and NoSQL world. I am practicing AWS' GSI and LSI. I am using serverless framework. I split my handlers multiple lambda function.I want to create my data where I can see all the restaurants and what type of beers they have its' price. Also I want to query all the beers without price. I successfully create the restaurant and it's partition key is id. I can able to get all the restaurant data. But I stuck in beer logic. I create api endpoint for beers like this restaurant/{id}/createBeers, when I made post request, I got error "message": "One or more parameter values were invalid: Missing the key id in the item" because it asked for restaurant's id. I cant find the logic where i can add the restaurant's id to create the beers and how to get all beer's without price.

The Restaurant is one to many. Beer is many to many(Same name different price based on Restaurant). This is what I want to achieve.

[
      {
        "id": 1,
        "name": "restaurent 1",
        "beers": {
            "tap-beers": [{
                    "name": "beer 1",
                    "price": "2$"
                },
                {
                    "name": "beer 2",
                    "price": "2$"
                }
            ],
            "bottle-beers": [{
                    "name": "beer 3",
                    "price": "2$"
                },
                {
                    "name": "beer 4",
                    "price": "2$"
                }
            ]
        }
    },
    
      {
        "id": 2,
        "name": "restaurent 2",
        "beers": {
            "tap-beers": [{
                    "name": "beer 1",
                    "price": "3$"
                },
                {
                    "name": "beer 2",
                    "price": "3$"
                }
            ],
            "bottle-beers": [{
                    "name": "beer 3",
                    "price": "4$"
                },
                {
                    "name": "beer 4",
                    "price": "6$"
                }
            ]
        }
    }
]

This is How I want get all the beers in the table

[
  {
    "beername": "beer 1"
  },
   {
    "beername": "beer 2"
  },
   {
    "beername": "beer 3"
  },
   {
    "beername": "beer 4"
  }
]

This is is my restaurant create

const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.createRestaurant = async event => {
  const resquestBody = JSON.parse(event.body);

  const params = {
    TableName: "table name",
    Item: {
      id: uuid.v1(),
      name: resquestBody.name,
      beers: [] // in here I will add beers when I can create a beer my post method path 
                // is restaurant/{id}/createBeers
    }
  }

  try {
    await dynamoDb.put(params).promise();
    return {
      statusCode: 200,
      body: JSON.stringify(resquestBody),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify(error),
    };
  }
};

This is create Beer handler

const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.createBeers = async event => {
  const requestBody = JSON.parse(event.body);
  const params = {
    TableName: "table name",
    Item: {
      beer_name: requestBody.beer_name,
      beer_type: requestBody.beer_type,
      beer_price: requestBody.beer_price
    }
  };

  try {
    await dynamoDb.put(params).promise();
    return {
      statusCode: 200,
      body: JSON.stringify(requestBody),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify(error),
    };
  }
};

This is my GSI get all beers without price

module.exports.getBeers = async event => {

  const params = {
    TableName: "beer",
    IndexName: "beers",
    KeyConditionExpression: "beer_name = :beer_name",
    ExpressionAttributeValues: {
      ":beer_name": "beer_name"
    },
    Limit: 1
  }

  try {
    let data = await dynamoDb.query(params).promise();
    return {
      statusCode: 200,
      body: JSON.stringify(data.Items),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify(error),
    };
  }

}

This is my serverless yml file

functions:
  createRestaurant:
    handler: handlers/createRestaurant.createRestaurant
    events:
      - http:
          path: restaurant
          method: post
          cors: true
  getRestaurants:
    handler: handlers/getRestaurants.getRestaurants
    events:
      - http:
          path: restaurant/all
          method: get
          cors: true
  createBeers:
    handler: handlers/createBeers.createBeers
    events:
      - http:
          path: restaurant/{id}/beers
          method: post
          cors: true
  getBeers:
    handler: handlers/getBeers.getBeers
    events:
      - http:
          path: beers/all
          method: get
          cors: true
resources:
  Resources:
    table:
      Type: "AWS::DynamoDB::Table"
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
          - AttributeName: beer_name
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        GlobalSecondaryIndexes:
          - IndexName: beers
            KeySchema:
              - AttributeName: beer_name
                KeyType: HASH
            Projection:
              ProjectionType: ALL
            ProvisionedThroughput:
              ReadCapacityUnits: 1
              WriteCapacityUnits: 1
        TableName: "tableName"

Upvotes: 0

Views: 530

Answers (1)

Maurice
Maurice

Reputation: 13108

Entities + Relationships

From what I gather you have a two entities:

  • Restaurant
    • ID
    • Name
  • Beer
    • Name

Each restaurant may have multiple beers and each bear is either bottled or tap and also has a price.

Access patterns

You want to enable these access patterns:

  1. Get a list of restaurants
  2. Get a list of beers per restaurant
  3. Get a list of beers in all restaurants

Table Layout

I propose a single table with a Global Secondary Index (GSI1) that looks like this:

Primary Table View

Primary Key Perspective

GSI1 Table View

GSI1 Perspective

How to query

  1. To get a list of all restaurants you do a query against GSI1 with PK=RESTAURANTS
import typing
import boto3
import boto3.dynamodb.conditions as conditions

def get_all_restaurants() -> typing.List[dict]:

    table = boto3.resource("dynamodb").Table("table-name")

    response = table.query(
        KeyConditionExpression=conditions.Key("GSI1PK").eq("RESTAURANTS"),
        IndexName="GSI1"
    )

    return response["Items"]
  1. To get a list of all beers in a given restaurant, you do a query against the primary index with PK=R<restaurant-id> and SK begins_with B (You can replace B with B-TP for a list of all tap beers or B-BB for all bottled beers. Get rid of the SK-condition for all info about the restaurant.)
import typing
import boto3
import boto3.dynamodb.conditions as conditions

def get_beers_in_restaurant(restaurant_id: str) -> typing.List[dict]:

    table = boto3.resource("dynamodb").Table("table-name")

    response = table.query(
        KeyConditionExpression=conditions.Key("PK").eq(f"R#{restaurant_id}") \
            & conditions.Key("SK").begins_with("B-")
    )

    return response["Items"]

def get_tap_beers_in_restaurant(restaurant_id: str) -> typing.List[dict]:

    table = boto3.resource("dynamodb").Table("table-name")

    response = table.query(
        KeyConditionExpression=conditions.Key("PK").eq(f"R#{restaurant_id}") \
            & conditions.Key("SK").begins_with("B-TB")
    )

    return response["Items"]

def get_bottled_beers_in_restaurant(restaurant_id: str) -> typing.List[dict]:

    table = boto3.resource("dynamodb").Table("table-name")

    response = table.query(
        KeyConditionExpression=conditions.Key("PK").eq(f"R#{restaurant_id}") \
            & conditions.Key("SK").begins_with("B-BB")
    )

    return response["Items"]
  1. To get a list of all beers, do a query against GSI1 with PK=BEERS. You will get duplicates, so you need to remove those client-side.
import typing
import boto3
import boto3.dynamodb.conditions as conditions

def get_all_beers() -> typing.List[dict]:

    table = boto3.resource("dynamodb").Table("data")

    response = table.query(
        KeyConditionExpression=conditions.Key("GSI1PK").eq("BEERS"),
        IndexName="GSI1"
    )
    
    list_with_duplicates = [item["name"] for item in response["Items"]]
    list_without_duplicates = list(set(list_with_duplicates))

    return [{"beername": name} for name in list_without_duplicates]

Adding items to the table

When you now add items to the table, you have to consider what type of entity you want to create.

Adding a Restaurant

Restaurants are pretty simple, they just need an ID and a name. From these information, we can compute the key-attributes:

import boto3

def create_restaurant(restaurant_id: str, name: str) -> None:
    table = boto3.resource("dynamodb").Table("data")

    item = {
        "PK": f"R#{restaurant_id}",
        "SK": "META",
        "GSI1PK": "RESTAURANTS",
        "GSI1SK": f"R#{restaurant_id}",
        "type": "RESTAURANT",
        "id": restaurant_id,
        "name": name
    }

    table.put_item(
        Item=item
    )

Add a beer to a restaurant

The beer always belongs to a restaurant, so we need to have it's id - it also always has a price and a name.

import boto3

def create_beer_for_restaurant(restaurant_id: str, name: str, price: str, is_tap: bool):

    table = boto3.resource("dynamodb").Table("data")

    sk = f"B-TB#{name}" if is_tap else f"B-BB#{name}"

    item = {
        "PK": f"R#{restaurant_id}",
        "SK": sk,
        "GSI1PK": "BEERS",
        "GSI1SK": f"B#{name}",
        "name": name,
        "price": price
    }

    table.put_item(
        Item=item
    )

Upvotes: 2

Related Questions