AWS Developer Tools Blog

Amazon DynamoDB Document API in Ruby (Part 2 – Condition Expressions)

As we showed in the previous post, it’s easy to put JSON items into Amazon DynamoDB and retrieve specific attributes with projection expressions. Condition Expressions provide a more flexible and SQL-like way to retrieve only the items you want from DynamoDB. First, let’s put a few more items into DynamoDB using a BatchWriteItem operation. (Note: this code uses the same ProductCatalog table we used in Part 1)

# add some more items
@dynamodb.batch_write_item(
  :request_items => {
    "ProductCatalog" => [

      {:put_request => { :item => {
        Id: 300,
        Title: "Sojourner",
        Description: "Mars Pathfinder robotic Mars rover",
        Price: BigDecimal.new("2.65e8"),
        LaunchDate: {
          M: 12, D: 4, Y: 1996
        },
        LostCommunicationDate: {
          M: 9, D: 27, Y: 1997
        },
        Features: {
          Rover: true,
        },
        NumberInStock: 10,
        OrdersPlaced: 3,
        Tags: ["#Mars", "#InStarTrekSeason4", "#InRedPlant2000", "#LostComms"],
      }}},

      {:put_request => { :item => {
        Id: 301,
        Title: "Spirit",
        Description: "Mars Exploration Rover – A",
        Price: BigDecimal.new("4.1e8"),
        LaunchDate: {
          M: 6, D: 10, Y: 2003
        },
        LostCommunicationDate: {
          M: 3, D: 22, Y: 2010
        },
        Features: {
          Rover: true,
        },
        NumberInStock: 10,
        OrdersPlaced: 5,
        Tags: Set.new(["#Mars", "#StuckOnMars", "#LostComms"]),
      }}},

      {:put_request => { :item => {
        Id: 302,
        Title: "Opportunity",
        Description: "Mars Exploration Rover – B",
        Price: BigDecimal.new("4.1e8"),
        LaunchDate: {
          M: 7, D: 7, Y: 2003
        },
        LostCommunicationDate: nil,
        Features: {
          Rover: true,
        },
        NumberInStock: 10,
        OrdersPlaced: 10,
        Tags: Set.new(["#Mars", "#StillRoving"]),
      }}},

      {:put_request => { :item => {
        Id: 303,
        Title: "Curiosity",
        Description: "car-sized robotic rover",
        Price: BigDecimal.new("2.5e9"),
        LaunchDate: {
          M: 11, D: 26, Y: 2011
        },
        LostCommunicationDate: nil,
        Features: {
          Rover: true,
          RoboticArm: true,
        },
        NumberInStock: 0,
        OrdersPlaced: 30,
        Tags: Set.new(["#Mars", "#MarsCuriosity", "#StillRoving"]),
      }}},

    ]
  }
)

Using Condition Expressions

We could also use condition expressions on the results of Query, but since we’re using a simple data model (only have hash key on product Id), we demonstrate this with scans. We use the following helper method to perform the scan and format the product titles returned:

def do_scan(filter_exp, exp_attribute_values)
  result = @dynamodb.scan(
    :expression_attribute_values => exp_attribute_values,
    :filter_expression => filter_exp,   # Condition Expressions are supplied through the FilterExpression parameter
    :projection_expression => "Title",
    :table_name => "ProductCatalog"
  ).data.items

  # format all retrieved titles into a single line
  return "scan retrieved: #{(result.map { |item| item["Title"] }).join(", ")}"
end

Let’s look at some example expressions and the results they return from our current ProductCatalog table:

# All products that don't have a launch month of November (11)
puts do_scan(
  "LaunchDate.M <> :m",
  {
    ":m" => 11
  }
)
# scan retrieved: 20-Bicycle 205, Opportunity, Spirit, Sojourner


# All rover products that don't have a launch month of November
puts do_scan(
  "attribute_exists(Features.Rover) AND LaunchDate.M <> :m",
  {
    ":m" => 11,
  }
)
# scan retrieved: Opportunity, Spirit, Sojourner


# Non-rovers
puts do_scan(
  "attribute_not_exists(Features.Rover)",
  nil
)
# scan retrieved: 20-Bicycle 205


# mid-range rovers or inexpensive products
puts do_scan(
  "(Price BETWEEN :low AND :high) OR Price < :verylow",
  {
    ":verylow" => BigDecimal.new("1e8"),
    ":low" => BigDecimal.new("3e8"),
    ":high" => BigDecimal.new("5e8")
  }
)
# scan retrieved: 20-Bicycle 205, Opportunity, Spirit


# within-Item referencing: more orders placed than in stock
puts do_scan(
  "OrdersPlaced > NumberInStock",
  nil
)
# scan retrieved: Curiosity


# string prefixing
puts do_scan(
  "begins_with(Title, :s)",
  {
    ":s" => "S",
  }
)
# scan retrieved: Spirit, Sojourner


# contains
puts do_scan(
  "contains(Tags, :tag1) AND contains(Tags, :tag2)",
  {
    ":tag1" => "#StuckOnMars",
    ":tag2" => "#LostComms",
  }
)
# scan retrieved: Spirit


# contains (Note: "Tags" is a list for Sojourner)
puts do_scan(
  "contains(Tags, :tag1)",
  {
    ":tag1" => "#LostComms",
  }
)
# scan retrieved: Spirit, Sojourner


# in operator
puts do_scan(
  "Id in (:id1, :id2)",
  {
    ":id1" => 302,
    ":id2" => 303,
  }
)
# scan retrieved: Curiosity, Opportunity


# equivalently, with parentheses
puts do_scan(
  "(Id = :id1) OR (Id = :id2)",
  {
    ":id1" => 302,
    ":id2" => 303,
  }
)
# scan retrieved: Curiosity, Opportunity

Next Steps

As you can see, condition expressions enable you to write more concise code to retrieve data. They also provide querying capabilities unavailable with the original access model such as within-Item references and more flexible conditions with parentheses. In an upcoming blog post, we’ll take a closer look at how we can update existing data through update expressions.