Clojure and DynamoDB with Faraday, Part 2

The story so far

On Part 1 we went over the basic operations - creating a table, checking its status, getting data in and performing a simple retrieval.

We’ll now look into various ways of retrieving items, including querying, scanning, and using projections to get only a few properties.

This assumes you’ve already completed part 1, since we’ll be using the data we added. As a reminder, I’m following the Javascript examples on basic DynamoDB operations, since they are the ones where the data set-up will more closely match Clojure. You may want to follow along for extra explanations and for comparison purposes.

Step 4: Read an Item Using Its Primary Key

The next step on DynamoDB’s Getting Started is to retrieve items by their key.

Read an item using get-item

We already skipped ahead a bit on the last part and saw how to use get-item. To refresh our memories, get-item receives a table and a map of the key to retrieve (using attribute and value).

1
2
(far/get-item client-opts :music {:artist "No One You Know"
:song-title "Call Me Today"})

Notice we need to use both the values for the key, and if your key is a string, it’ll be case-sensitive.

It returns the item as a map, including all attributes.

1
2
3
4
5
6
7
{:artist "No One You Know",
:year 2015N,
:price 2.14M,
:genre "country",
:song-title "Call Me Today",
:album-title "Somewhat Famous",
:tags {:composers ["Smith" "Jones" "Davis"], :length-in-seconds 214N}}

Retrieve a subset of attributes

DynamoDB also allows us to retrieve only a few attributes. To retrieve only the album title and year:

1
2
3
(far/get-item client-opts :music {:artist "No One You Know"
:song-title "Call Me Today"}
{:attrs [:album-title :year]})

Which gets us a smaller map.

1
{:year 2015N, :album-title "Somewhat Famous"}

Retrieve a subset of attributes using a projection expression

We used a legacy parameter up there instead of an expression, though. Expressions are a lot more flexible than just sending an attribute list, but have some restrictions.

Mainly, when using expressions:

  • You can’t have attribute names with dashes, and
  • You can’t use reserved words.

We are hitting both cases on the previous call. album-title has a dash on the name, and year is a reserved word.

The way around that is to add a placeholder token on the expression that stands in for the attribute, and then adding the attribute name on the expression attribute values.

The equivalent call to the previous example, using a projection expression, would be:

1
2
3
4
5
6
7
(far/get-item client-opts :music
{:artist "No One You Know"
:song-title "Call Me Today"}
{:proj-expr "#a, #y"
:expr-attr-names {"#y" "year"
"#a" "album-title"}
})

That would seem rather cumbersome if we can simply request the attributes, but the next example will give you a better idea of what you can do with expressions.

Retrieve nested attributes using path notation

You’ll recall that when we created the item, we added a complex attribute :tags which was itself a map.

1
2
3
4
5
6
7
8
{:artist "No One You Know"
:song-title "Call Me Today"
:album-title "Somewhat Famous"
:year 2015
:price 2.14
:genre :country
:tags {:composers ["Smith" "Jones" "Davis"]
:length-in-seconds 214}}

On a projection expression, we can use path notation to retrieve only some attributes inside :tags, even down to requesting a specific item inside the :composers vector.

Like before, we’ll need to use expression attributes for those elements that have a dash in the name.

1
2
3
4
5
6
7
8
(far/get-item client-opts :music
{:artist "No One You Know"
:song-title "Call Me Today"}
{:proj-expr "#a, #y, tags.composers[0], tags.#l"
:expr-attr-names {"#y" "year"
"#a" "album-title"
"#l" "length-in-seconds"}
})

This returns only those attributes we requested, retaining their original nesting.

1
2
3
4
{:year 2015N,
:album-title "Somewhat Famous",
:tags {:composers ["Smith"],
:length-in-seconds 214N}}

Read multiple items using batch-get-item

We can also retrieve multiple items in a single call by their primary key, using batch-get-item. Like we saw on the call to batch-write-item (part 1), we can also requests items from multiple tables by adding a request for each one to the map.

1
2
3
4
5
6
(far/batch-get-item client-opts
{:music {:prim-kvs [{:artist "No One You Know" :song-title "My Dog Spot"}
{:artist "No One You Know" :song-title "Somewhere Down The Road"}
{:artist "The Acme Band" :song-title "Still In Love"}
{:artist "The Acme Band" :song-title "Look Out, World"}]
:attrs [:promotion-info :critic-rating :price]}})

This results on us just getting the relevant attributes for those elements, organized by table. If a requested attribute isn’t preset on the item, there will not be a corresponding key on the return map.

1
2
3
4
5
6
7
{:music [{:price 0.99M}
{:price 1.98M, :critic-rating 8.4M}
{:critic-rating 9.4M}
{:price 2.47M,
:promotion-info {:radio-stations-playing ["KHCR" "KBQX" "WTNR" "WJJH"],
:rotation "heavy",
:tour-dates {:Seattle "20150625", :Cleveland "20150630"}}}]}

You can’t assume that the items will be returned on the same order as the request, so unlike in the example above you’ll want to include the key as well (I left it out to directly reproduce Amazon’s example).

Step 5: Query and Scan the Table

We can also do queries and scans on tables. The syntax for these will be very similar to the one we have been using up until now.

Run a query

We can query on a table using its partition key.

1
(far/query client-opts :music {:artist [:eq "No One You Know"]})

This will return all items, sorting by the range key (the song’s title).

We can also filter using key attributes, and ask DynamoDB to return only a few of them. To get only songs for “The Acme Band” where the title starts with a character:

1
2
3
(far/query client-opts :music {:artist [:eq "The Acme Band"]
:song-title [:begins-with "S"]}
{:return [:song-title]})

Filtering query results

Querying looks for items based on its primary key (or a fraction of it). We can also use filter expressions to filter out some items from those that match, using even complex expressions and nested values.

On the following example, we’ll need to use both expression attribute values and expression attribute names, since they have the same limitations as the expressions we used for projections, and we can’t reference on them attributes with dashes on the name.

1
2
3
4
5
6
7
8
(far/query client-opts :music {:artist [:eq "The Acme Band"]
:song-title [:begins-with "S"]}
{:filter-expr "size(#p.#rs) >= :howmany"
:proj-expr "#s, #p.rotation"
:expr-attr-names {"#p" "promotion-info"
"#rs" "radio-stations-playing"
"#s" "song-title"}
:expr-attr-vals {":howmany" 4}})

Instead of adding the filtering values directly on the filter expression, we used an expression attribute values map - passed via :expr-attr-vals - to provide them.

This query results on a vector with the requested attributes for the single matching item.

1
[{:promotion-info {:rotation "heavy"}, :song-title "Still In Love"}]

Scan the table

Finally, we can do always do a full table scan (which will likely get expensive!).

1
(far/scan client-opts :music)

scan supports filter and projection expressions, just like query, but you don’t need to know the primary key in order to do it.

Next steps

That’s it for retrieving, querying and scanning. On part 3 we’ll look at maintaining and working with secondary indexes. Until then!

Author

...
Ricardo J. Méndez