DynamoDB Dynomite Migrations Create Table Examples

Single Parition Key

You can use dynamodb:generate to create a starter migration file.

❯ jets dynamodb:generate create_posts
Migration file created: dynamodb/migrate/20230728152326-create_posts.rb

dynamodb/migrate/20230728152326-create_posts.rb

class CreatePosts < Dynomite::Migration
  def up
    create_table :posts do |t|
      t.partition_key :id # post-A2uxLhRzIYdqWwtN
      # Creating GSI with create_table is much faster than update_table. 20s vs 5m to 10m
      t.add_gsi :updated_at
    end
  end
end

The type of migration “create_table” is since the provided name, create_posts, starts with create. Also, in this example, the primary_key is the partition_key id. The id must be unique since only the partition_key is used.

Composite Key

Here’s an example where the primary_key is a “composite key”. It has both partition_key and sort_key.

❯ jets dynamodb:generate create_products
Migration file created: dynamodb/migrate/20230728152326-create_products.rb

dynamodb/migrate/20230728152326-create_products.rb

class CreateProducts < Dynomite::Migration
  def up
    create_table :products do |t|
      t.partition_key "category:string" # required
      t.sort_key "sku:number"    # both category + sku identifies uniqueness
      # Should add an index for the id field still
      t.add_gsi :id # product-A2uxLhRzIYdqWwtN
      t.add_gsi partition_key: :category, sort_key: :updated_at # product-A2uxLhRzIYdqWwtN
    end
  end
end

If the Primary Key is a Composite Key, then the combination identifies uniqueness. It’s still useful to have an indexed id to lookup with one field. Dynomite always saves an id field for easy lookup.

Here are a few more create examples:

jets dynamodb:generate create_products --partition-key category --sort-key sku:number
jets dynamodb:generate create_posts --partition-key id # default attribute type is string
jets dynamodb:generate create_comments --partition-key post_id:string --sort-key created_at:string

Primary Key: Partition Key Only or Composite Key with Sort Key

If you’re coming from the ActiveRecord, in the relational database world, a “primary key” is a single column that identifies uniqueness. With DynamoDB, there’s a primary key also, but you can choose between 2 types of primary keys:

Primary Key Types:

  1. Parition Key Only: One Field Only. This field value must be unique within the table.
  2. Composite Key: Partition Key + Sort Key. The combination of these fields must be unique within the table.

In general, I prefer creating tables with a Partition Key only because:

  • Partition Key Only primary keys are easier to understand. You use it the same conceptually so you would with relational database. This reduce mental overhead makes a difference.
  • When querying, with a Primary Key Partition Key Only, you only need to provide one value get the item. With a Composite Key, you have to provide both field values. Even though it’s just one extra field, the interface is more cumbersome.
  • You can always create a GSI index that is a Composite Key. You can use this to group “categories” and a sort column for filtering together.
  • Associations work faster and better for tables with only a Partition Key. Only the Partition Key is stored in the “foreign key” column that Dynomite manages.

When you use a partition_key only, the partition_key identifies uniqueness.

With a “composite key”, the partition_key acts like a “category”. Both the partition_key and sort_key are needed to identify uniqueness. A “composite key” is an abstract concept and only exists a result of specifying a sort_key during table creation. When you have a sort key, you will always have a composite key.

This article also has a useful explanation of DynamoDB’s partition, sort, and compose keys: DynamoDB Composite Key - The Ultimate Guide

When you have a composite key on your DynamoDB Table, that will be considered the primary key. In simpler terms, this means when you have a sort key set on your table, all Get, Update, Delete item commands must include both the partition key and sort key. This is because the partition key is no longer considered unique, the composite key is considered unique, and therefore you need both elements to identify the specific item you are referring to.

Local Secondary Index

class CreateProducts < Dynomite::Migration
  def up
    create_table :products do |t|
      t.partition_key "category:string" # required
      t.sort_key  "post_id:number"      # makes primary_key a composite key
      t.add_lsi("name:string")  # used as sort key, the partition key is inferred
      # t.add_lsi(sort_key: "name:string") # also works
    end
  end
end

Using key_schema and attribute_definitions

Here’s an example where of using key_schema and attribute_definitions directly.

class CreateComments < Dynomite::Migration
  def up
    create_table :comments do |t|
      # Instead of using partition_key and sort_key you can set the
      # key schema directly also
      t.key_schema([
          {attribute_name: "id", :key_type=>"HASH"},
          {attribute_name: "created_at", :key_type=>"RANGE"}
        ])
      t.attribute_definitions([
        {attribute_name: "id", attribute_type: "N"},
        {attribute_name: "created_at", attribute_type: "S"}
      ])

      # set the billing mode to on-demand
      # t.billing_mode(:pay_per_request) # default for dynomite

      # Also other ways to set provisioned_throughput
      # t.provisioned_throughput(:read, 10)
      # t.provisioned_throughput(:write, 10)
      # t.provisioned_throughput(
      #   read_capacity_units: 5,
      #   write_capacity_units: 5
      # )
    end
  end
end

Global Secondary Index

Here’s an example where we create GSI indexes as part of table creation.

class CreateComments < Dynomite::Migration
  def up
    create_table :comments do |t|
      t.partition_key :post_id # required
      t.sort_key  :created_at # optional
      t.add_gsi :updated_at
    end
  end
end

You can think of GSIs as copies of the original table.