Dynamo::Record

Provides helpful rake tasks and model extensions on top of aws-record.

Installation

Add this line to your application's Gemfile:

gem 'dynamo-record'

And then execute:

$ bundle

Or install it yourself as:

$ gem install dynamo-record

Usage

Models

In app/models, create a class that includes Dynamo::Record::Model and contains one or more of the standard set of attribute declarations, along with the following new composite attribute types:

  • composite_string_attr
  • composite_integer_attr

An example file:

class MyModel
  include Dynamo::Record::Model
  composite_string_attr(
    :model_key,
    hash_key: true,
    parts: [:model_id, :account_id]
  )
  integer_attr :position, range_key: true
  string_attr  :user_id
  float_attr   :score
  map_attr     :map_value
end

The partition key is labeled with hash_key: true. The sort key is labeled with range_key: true.

Declaring secondary indexes are included in aws-record and are defined here.

As an example, a global secondary index can be defined as follows:

  global_secondary_index(
    :user_idx,
    hash_key: :user_id,
    range_key: :score,
    projection: {
      projection_type: 'INCLUDE',
      non_key_attributes: [
        :map_value
      ]
    }
  )

As an example, a local secondary index can be defined as follows:

  local_secondary_index(
    :score_idx,
    range_key: :score,
    projection: {
      projection_type: 'INCLUDE',
      non_key_attributes: [
        :map_value
      ]
    }
  )

The full documentation for projection_type and non_key_attributes can be found here.

Query Helpers

Query helpers are included as class methods.

  • composite_key(*args)

For models that have composite attributes, create a composite key value from the individual attribute parts:

hash_key = MyModel.composite_key('model_1', 'account_1')
  • split_composite(string)

Split a composite attribute value into its individual attribute parts:

model_id, acccount_id = MyModel.split_composite(hash_key)
  • find_all_by_hash_key(hash_key_value, opts = {})

Find all item instances that match a given hash key value. opts are options passed to the underlying query.

MyModel.find_all_by_hash_key(hash_key)
  • find_all_by_gsi_hash_key(gsi_name, hash_key_value, opts = {})

Find all item instances using a global secondary instance using a hash key value. opts are options passed to the underlying query.

MyModel.find_all_by_gsi_hash_key('user_idx', 'user_1')
  • find_all_by_gsi_hash_and_range_keys(gsi_name, hash_key_value, range_key_value)

Find all item instances using a global secondary instance using a hash key value and range key value.

MyModel.find_all_by_gsi_hash_key('user_idx', 'user_1', 0.0)
  • find_all_by_lsi_hash_key(lsi_name, hash_key_value, opts = {})

Find all item instances using a local secondary instance using a hash key value. opts are options passed to the underlying query.

MyModel.find_all_by_lsi_hash_key('score_idx', 'user_1')
  • find_all_by_lsi_hash_and_range_keys(lsi_name, hash_key_value, range_key_value)

Find all item instances using a local secondary instance using a hash key value and range key value.

MyModel.find_all_by_lsi_hash_key('score_idx', 'user_1', 0.0)

Migrations

Dynamo migration files are stored in db/dynamo_migrate. The name of the file follows the style of standard Rails migration files like YYYYMMDDHHMMSS_create_model_1.rb. A migration file that creates a Dynamo table looks like:

module DynamoMigrate
  class CreateMyModel < Dynamo::Record::TableMigration
    def self.up
      migrate(MyModel) do |migration|
        migration.create!(
          provisioned_throughput: {
            read_capacity_units: 1,
            write_capacity_units: 1
          },
          global_secondary_index_throughput: {
            user_idx: {
              read_capacity_units: 1,
              write_capacity_units: 1
            }
          }
        )
      end
    end
  end
end

Note that the throughput of the table can be configured separately from the throughput of the secondary index.

A migration file that creates a Dynamo stream looks like:

module DynamoMigrate
  class AddMyModelStream < Dynamo::Record::TableMigration
    def self.update
      add_stream(MyModel)
    end
  end
end

Development

A simple docker environment has been provided for spinning up and testing this gem with multiple versions of Ruby. This requires docker and docker-compose to be installed. To get started, run the following:

./build.sh

This will install the gem in a docker image with all versions of Ruby installed, and install all gem dependencies in the Ruby 2.6 set of gems. It will also download and spin up a DynamoDB Local container for use with specs. Finally, it will run wwtd, which runs all specs across all supported version of Ruby and Rails, bundling gems for each combination along the way.

The first build will take a long time, however, docker images and gems are cached, making additional runs significantly faster.

Individual spec runs can be started like so:

docker-compose run --rm app /bin/bash -l -c \
  "BUNDLE_GEMFILE=spec/gemfiles/rails-5.2.gemfile rvm-exec 2.5 bundle exec rspec"

If you'd like to mount your git checkout within the docker container running tests so changes are easier to test, use the override provided:

cp docker-compose.override.example.yml docker-compose.override.yml

Making a new Release

To release a new version, update the version number in version.rb, and then just run gem build dynamo-record, and push the .gem file to rubygems.org. To install this gem onto your local machine, run gem install dynamo-record-*.gem.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/instructure/dynamo-record.

License

The gem is available as open source under the terms of the MIT License.