I spent a little time on Dynomite as part of working on the huge Jets 5 Release. This led to a Dynomite 2 release that’s ActiveModel Compatible.

In my first attempt at Dynomite 1, I thought that because DynamoDB is a key-value database, not a relational database, the interface would need to be different to reflect that. However, the more I used the v1 interface, the more I missed the ActiveModel interface. ActiveModel is simply too addicting.

Dynomite 2 is my humble attempt at making a DynamoDB ActiveRecord-like ORM library. Thanks to Rails abstracting the interface into ActiveModel, Dynomite can include the ActiveModel modules and define the required interface methods to make itself ActiveRecord-like.

Features

Let’s get into the meat and potatoes.

  • ActiveRecord or ActiveModel Compatibility: Welcome back to ActiveRecord familarity. You get validations, callbacks, dirtiness, etc. You have familiar methods like find and find_by.
  • Relation Chainable Querying: Querying can be done with a chainable where method. You can do things like all.where.limit.scan_index_backward. I’ll cover a few examples below.
  • Simple migrate command: In v1, you had to specify the path of the migration file. This wasn’t very pleasant. In v2, the command is simple again: jets dynamodb:migrate. The state of whether or not a migration has been ran is now stored in a schema_migrations table.
  • Indexes are Automatic: Indexes are discovered instead of defined by the model. This allows the querying interface to stay simple. It also allows any indexes added to be used automatically. You don’t have to change the code. It’s a more normal database workflow. IE: with MySQL, if you add an index, it automatically gets used. Imagine having to specify you’re always having to specify your indexes with SQL 🤣 Yes, explicitly. Pain. 😅
  • Documentation: There’s improved documentation also. They are a part of the jets docs: Dynomite Docs

We’ll cover a few of the highlights in a little more detail next.

ActiveModel Compatibility

Dynomite is ActiveModel compatible. Thanks to this, you can use things like validations and callbacks. Here’s an example:

app/models/product.rb

class Product < ApplicationItem
  field :category,
        :product_id,
        :product_name,
        :price,
        :stock_quantity

  validates :price, presence: true
  before_save :set_stock_quantity

  def set_stock_quantity
    self.stock_quantity ||= 1
  end
end

Querying Examples

Here are some chain query examples.

Post.all.where(title: "title 1").limit(1).first
Post.all.where(title: "title 1").count

Note that the return object of querying methods like all, where, and limit is a lazy Enumerator Relation. Since it’s lazy, AWS API calls will not be made to the DynamoDB service until the records are needed. Let’s take a closer look:

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

The Relation object contains attributes that will be used to run the 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"}]}>

You can then chain additional methods. More Docs: Querying Where

DynamoDB Dynomite Comparisons Functions and Querying Expressions

You can use comparison options like lt and gt. Here are some examples:

Product.where(category: "Electronics").and("price.lt": 1000)
Product.where(category: "Electronics").and.where("price.lt": 1000)
Product.where(category: "Electronics").and.where.not("price.lt": 1000)

You can also use DynamoDB native “functions”.

Product.where(category: "Electronics").attribute_exists('pictures.slide_view')
Product.attribute_exists('pictures.slide_view')
Product.attribute_not_exists('pictures.slide_view')
Product.attribute_type('pictures.slide_view', :string)
Product.begins_with("category", "Elect") # works for String
Product.contains("tags", "foo") # works for String, Set and List
Product.size_fn("category.gt", 100)

More info Querying Expressions

Find By

The familiar find_by is available.

Product.find_by(category: "Electronics", sku: 101)
Product.find_by(category: "Electronics")
Product.find_by(category: "Electronics", sku: 101, name: "Smartphone", price: 500, stock_quantity: 50)
Product.find_by(price: 500, stock_quantity: 50)

Note that Dynomite will discover indexes and use them when available.

More info find_by docs

Assocations

Associations are also supported.

app/models/user.rb

class User < ApplicationItem
  has_many :posts
end

There is typically a corresponding belongs_to as the inverse relationship.

app/models/post.rb

class Post < ApplicationItem
  belongs_to :user
end

Note, there are some limits with associations since it’s not implemented with join tables, but as a field in the existing tables. Remember, DynamoDB is a document-based database, not a relational database.

More info Associations Docs

Summary

Dynomite 2 is a significant update. The major updates are ActiveModel Compatibility and Relation Chainable Querying. The migrate command is also simplified. The automatic behavior, like indexes, makes using DynamoDB easier. There’s even association support. For more information, check out the Dynomite Docs.

I also wanted to note that I did study and try a few other libraries. They were not for me. Here are comparisons of them here: Dynomite Vs Others