DynamoDB Dynomite Querying Where

Important: These docs are for the outdated Jets 5 versions and below. For the latest Jets docs: docs.rubyonjets.com

Querying is Lazy

We’ll start with the common method where and introduce the resulting Relation object. Dynomite querying methods are generally lazy. It returns a lazy Enumerator Relation object. This allows you to build up the query expression before making the DynamoDB API calls to load the items. Example:

 jets console
> Post.where(title: "title 1").class
=> Dynomite::Item::Query::Relation

The Relation object contains attributes to query.

> Post.where(title: "title 1")
=> #<Dynomite::Item::Query::Relation:0x00007f615b4fcea0 @query={:where=>[{:title=>"title 1"}]}>

You can chain the where methods.

> Post.where(title: "title 1").where(desc: "desc 1")
=> #<Dynomite::Item::Query::Relation:0x00007f615dc1b270 @query={:where=>[{:title=>"title 1"}, {:desc=>"desc 1"}]}>

The and method is aliased to where. These are all the same.

Post.where(title: "title 1").where(desc: "desc 1")
Post.where(title: "title 1").and(desc: "desc 1")
Post.where(title: "title 1", desc: "desc 1")

Ruby Enumerable Compatibility

Since the Relation object is an Enumerable object, you can use Ruby methods to iterate, traverse, and manipulate the collection of DynamoDB items. Ruby Enumerable methods:

each map select reject reduce any? all? sort count include?

Items are loaded at the moment those methods are used. IE: You do something like iterate through the collection with .each or call .first. You can also force the lazy Enumerator to load with .to_a or .force. Here are ways to load the actual records.

Post.where(title: "title 1").each { |post| p post }
Post.where(title: "title 1").first
Post.where(title: "title 1").to_a
Post.where(title: "title 1").force
Post.where(title: "title 1").count

And an example with some output:

> Post.where(title: "title 1").first
=> #<Post:0x00007f615b6f7e58 @attrs={"updated_at"=>"2023-07-28T00:30:49Z", "created_at"=>"2023-07-28T00:30:49Z", "id"=>"f74de472", "title"=>"title 1", "desc"=>"desc 1"}, @new_record=false>

Indexes Automatically Used

If there’s an index on one of the fields, it’s automatically discovered and used as part of a fast query operation. Example:

> Post.index_names
=> ["title-index"]
> Post.where(title: "title 1").first # title-index is automatically used

The first index that is discovered is used.

The order of index precedence is:

  1. Primary Key (Partition Key and Sort Key)
  2. LSI: Local Secondary Indexes
  3. GSI: Global Secondary Indexes

Primary Key is Composite Key: Partition and Sort Key

When there is a primary key that is a composite key (both partition_key and sort_key), both keys need to be provided to avoid a slow scan operation. Example:

Product.where(category: "Electronics", price: 100).to_params

Since the parition_key is category and sort_key is sku, the composite key index cannot be used. If there are individual GSI indexes on either field like category or price, then those one indexes will be used though.

See to_params

Dynomite works by building up the query in memory and then calling to_params before sending the request to DynamoDB. You can see the params and debug by calling the to_params method. It can be useful to see what parameters will be sent to the DynamoDB Ruby SDK API.

> Product.where(category: "Electronics", sku: 101).to_params
=> {:expression_attribute_names=>{"#category"=>"category", "#sku"=>"sku"},
 :expression_attribute_values=>{":category"=>"Electronics", ":sku"=>101},
 :key_condition_expression=>"#category = :category AND #sku = :sku",
 :table_name=>"demo-dev_products"}
>

Low-Level Methods

If you need more control over querying, you can drop down to the low-level wrapper scan and query methods. See: Low-Level Query Methods.