Cassandra を別の技術 (フルマネージドサービス) で置き換えるべく模索中.

資料

チュートリアル

ベストプラクティス

SDK

Cassandra との比較

準備

DynamoDB ローカルの導入

ローカル環境で DynamoDB を動かすために DynamoDB (ダウンロード可能バージョン) を利用する.

mkdir dynamodb_local && cd $_
wget -O - 'https://s3-ap-northeast-1.amazonaws.com/dynamodb-local-tokyo/dynamodb_local_latest.tar.gz' | tar zxf -
java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb

Rails

ruby -v
ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-darwin16]
mkdir dynamodb && cd $_
# Gemfile

source 'https://rubygems.org'

gem 'rails', '5.0.2'
bundle install --path vendor/bundle --jobs=4
bundle exec rails new .
# Gemfile

+gem 'aws-sdk', '~> 2'
+gem 'aws-sdk-rails'
+gem 'aws-record'

+gem 'dotenv-rails'
bundle install
# config/aws.yml

default: &default
  access_key_id: <%= ENV["AWS_ACCESS_KEY_ID"] %>
  secret_access_key: <%= ENV["AWS_SECRET_ACCESS_KEY"] %>
  region: <%= ENV["AWS_REGION"] %>

development:
  <<: *default
  endpoint: http://localhost:8000

test:
  <<: *default
  endpoint: http://localhost:8000

production:
  <<: *default
# config/initializers/aws.rb

Aws.config.update(Rails.application.config_for(:aws).symbolize_keys)
# .env
AWS_ACCESS_KEY_ID='your_access_key_id'
AWS_SECRET_ACCESS_KEY='your_secret_access_key'
AWS_REGION='local'

チュートリアル

Ruby および DynamoDB - Amazon DynamoDB に沿って進める.映画データを格納するテーブルを作成し,一通り CRUD の動作を確認する.

テーブルを作成する

# app/models/movie.rb

class Movie
  include Aws::Record

  integer_attr :year,  hash_key:  true
  string_attr  :title, range_key: true
  map_attr     :info
end
bundle exec rails g migration create_movies

マイグレーションファイルを Aws::Record を利用する形に書き換える.注意点として, Aws::Record を利用した場合は DynamoDB に作成されるテーブル名がモデル名の複数形ではなく単数形となり Rails の作法から外れる.テーブル名を複数形とするためには Aws::Record よりも低級な Aws::DynamoDB を用いる必要がある.

例ではモデル名が Movie であるため, Rails の作法に従うならテーブル名は movies となる.しかし Aws::Record を利用すると,モデル名と同じ movie というテーブルが作成される

# db/migrate/20170307124944_create_movies.rb

class CreateMovies < ActiveRecord::Migration[5.0]
  def up
    migration = Aws::Record::TableMigration.new(Movie)
    migration.create!(
      provisioned_throughput: {
        read_capacity_units: 5,
        write_capacity_units: 2
      }
    )
    migration.wait_until_available
  end

  def down
    raise ActiveRecord::IrreversibleMigration
  end
end
bundle exec rails db:migrate

サンプルデータをロードする

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/gettingstartedguide/GettingStarted.Ruby.02.html

curl -O 'http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/gettingstartedguide/samples/moviedata.zip'
unzip moviedata.zip
rm moviedata.zip
mv moviedata.json db/
# Gemfile

+gem 'seed-fu'
mkdir db/fixtures
# db/fixtures/movies.rb

file = File.read('db/moviedata.json')
movies = JSON.parse(file)
movies.each do |movie|
  Movie.new(movie).save!
end
bundle exec rails db:seed_fu
irb> Movie.find(year: 2013, title: 'Rush')
[Aws::DynamoDB::Client 200 0.048101 0 retries] get_item(table_name:"Movie",key:{"year"=>{n:"2013"},"title"=>{s:"Rush"}})

=> #<Movie:0x007ffeb82276f8 @data=#<Aws::Record::ItemData:0x007ffeb63b0f08 @data={:year=>0.2013e4, :title=>"Rush", :info=>{"actors"=>["Daniel Bruhl", "Chris Hemsworth", "Olivia Wilde"], "release_date"=>"2013-09-02T00:00:00Z", "plot"=>"A re-creation of the merciless 1970s rivalry between Formula One rivals James Hunt and Niki Lauda.", "genres"=>["Action", "Biography", "Drama", "Sport"], "image_url"=>"http://ia.media-imdb.com/images/M/MV5BMTQyMDE0MTY0OV5BMl5BanBnXkFtZTcwMjI2OTI0OQ@@._V1_SX400_.jpg", "directors"=>["Ron Howard"], "rating"=>0.83e1, "rank"=>0.2e1, "running_time_secs"=>0.738e4}}, @clean_copies={:year=>2013, :title=>"Rush", :info=>{"actors"=>["Daniel Bruhl", "Chris Hemsworth", "Olivia Wilde"], "release_date"=>"2013-09-02T00:00:00Z", "plot"=>"A re-creation of the merciless 1970s rivalry between Formula One rivals James Hunt and Niki Lauda.", "genres"=>["Action", "Biography", "Drama", "Sport"], "image_url"=>"http://ia.media-imdb.com/images/M/MV5BMTQyMDE0MTY0OV5BMl5BanBnXkFtZTcwMjI2OTI0OQ@@._V1_SX400_.jpg", "directors"=>["Ron Howard"], "rating"=>0.83e1, "rank"=>0.2e1, "running_time_secs"=>0.738e4}}, @dirty_flags={}, @model_attributes=#<Aws::Record::ModelAttributes:0x007ffeb81095f0 @model_class=Aws::Record::Attributes, @attributes={:year=>#<Aws::Record::Attribute:0x007ffeb8022ab0 @name=:year, @database_name="year", @dynamodb_type="N", @marshaler=#<Aws::Record::Marshalers::IntegerMarshaler:0x007ffeb8041f50>, @persist_nil=nil>, :title=>#<Aws::Record::Attribute:0x007ffeb8011170 @name=:title, @database_name="title", @dynamodb_type="S", @marshaler=#<Aws::Record::Marshalers::StringMarshaler:0x007ffeb8011260>, @persist_nil=nil>, :info=>#<Aws::Record::Attribute:0x007ffeb5b34118 @name=:info, @database_name="info", @dynamodb_type="M", @marshaler=#<Aws::Record::Marshalers::MapMarshaler:0x007ffeb5b341e0>, @persist_nil=nil>}, @storage_attributes={"year"=>:year, "title"=>:title, "info"=>:info}>, @track_mutations=true>>

項目を作成,読み込み,更新,削除する

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/gettingstartedguide/GettingStarted.Ruby.03.html

項目を作成する.プライマリキーを指定する.

irb> movie = Movie.new(year: 2015, title: 'The Big New Movie', info: { plot: 'Nothing happens at all.', rating: 0 })

irb> movie.save!
[Aws::DynamoDB::Client 200 0.059198 0 retries] put_item(table_name:"Movie",item:{"year"=>{n:"2015"},"title"=>{s:"The Big New Movie"},"info"=>{m:{"plot"=>{s:"Nothing happens at all."},"rating"=>{n:"0"}}}},condition_expression:"attribute_not_exists(#H) and attribute_not_exists(#R)",expression_attribute_names:{"#H"=>"year","#R"=>"title"})

=> #<struct Aws::DynamoDB::Types::PutItemOutput attributes=nil, consumed_capacity=nil, item_collection_metrics=nil>

項目を読み込む.プライマリキーを指定する.

irb> Movie.find(year: 2015, title: 'The Big New Movie')
[Aws::DynamoDB::Client 200 0.015147 0 retries] get_item(table_name:"Movie",key:{"year"=>{n:"2015"},"title"=>{s:"The Big New Movie"}})

=> #<Movie:0x007fe6d92a81f0 @data=#<Aws::Record::ItemData:0x007fe6d92a8150 @data={:year=>0.2015e4, :title=>"The Big New Movie", :info=>{"rating"=>0.0, "plot"=>"Nothing happens at all."}}, @clean_copies={:year=>2015, :title=>"The Big New Movie", :info=>{"rating"=>0.0, "plot"=>"Nothing happens at all."}}, @dirty_flags={}, @model_attributes=#<Aws::Record::ModelAttributes:0x007fe6d93726d0 @model_class=Aws::Record::Attributes, @attributes={:year=>#<Aws::Record::Attribute:0x007fe6d926b3b8 @name=:year, @database_name="year", @dynamodb_type="N", @marshaler=#<Aws::Record::Marshalers::IntegerMarshaler:0x007fe6d9289908>, @persist_nil=nil>, :title=>#<Aws::Record::Attribute:0x007fe6d925b1e8 @name=:title, @database_name="title", @dynamodb_type="S", @marshaler=#<Aws::Record::Marshalers::StringMarshaler:0x007fe6d925b2d8>, @persist_nil=nil>, :info=>#<Aws::Record::Attribute:0x007fe6d654aaf0 @name=:info, @database_name="info", @dynamodb_type="M", @marshaler=#<Aws::Record::Marshalers::MapMarshaler:0x007fe6d654abb8>, @persist_nil=nil>}, @storage_attributes={"year"=>:year, "title"=>:title, "info"=>:info}>, @track_mutations=true>>

項目を更新する.プライマリキーと更新したい属性を指定する.

irb> Movie.update(year: 2015, title: 'The Big New Movie', info: { plot: 'Everything happens all at once.', rating: 5.5, actors: ['Larry', 'Moe', 'Curly'] })
[Aws::DynamoDB::Client 200 0.086546 0 retries] update_item(table_name:"Movie",key:{"year"=>{n:"2015"},"title"=>{s:"The Big New Movie"}},update_expression:"SET #UE_A = :ue_a",expression_attribute_names:{"#UE_A"=>"info"},expression_attribute_values:{":ue_a"=>{m:{"plot"=>{s:"Everything happens all at once."},"rating"=>{n:"5.5"},"actors"=>{l:[{s:"Larry"},{s:"Moe"},{s:"Curly"}]}}}})

=> #<struct Aws::DynamoDB::Types::UpdateItemOutput attributes=nil, consumed_capacity=nil, item_collection_metrics=nil>

項目を削除する.削除したい項目と同じプライマリキーを持った項目を作成して削除する.

irb> movie = Movie.new(year: 2015, title: 'The Big New Movie')
irb> movie.delete!
[Aws::DynamoDB::Client 200 0.121163 0 retries] delete_item(table_name:"Movie",key:{"year"=>{n:"2015"},"title"=>{s:"The Big New Movie"}})

=> true

データをクエリおよびスキャンする

パーティションキーの指定が必須であり,ソートキーはオプションである.

クエリ - 1 年間にリリースされたすべての映画

1985 年にリリースされたすべての映画を取得する.プライマリキーの属性を指定して検索できる.

irb> movies = Movie.query(
irb*   key_condition_expression: '#yr = :yyyy',
irb*   expression_attribute_names: { '#yr' => 'year' },
irb*   expression_attribute_values: { ':yyyy' => 1985 })
=> #<Aws::Record::ItemCollection:0x007fb27d564cf8 @search_method=:query, @search_params={:key_condition_expression=>"#yr = :yyyy", :expression_attribute_names=>{"#yr"=>"year"}, :expression_attribute_values=>{":yyyy"=>1985}, :table_name=>"Movie"}, @model=Movie, @client=#<Aws::DynamoDB::Client>>

irb> movies.each do |movie|
irb*   puts "#{movie.year.to_i} #{movie.title}"
irb> end
[Aws::DynamoDB::Client 200 0.697524 0 retries] query(key_condition_expression:"#yr = :yyyy",expression_attribute_names:{"#yr"=>"year"},expression_attribute_values:{":yyyy"=>{n:"1985"}},table_name:"Movie")

1985 A Nightmare on Elm Street Part 2: Freddy's Revenge
1985 A Room with a View
1985 A View to a Kill
.
.
.
1985 The Return of the Living Dead
1985 Weird Science
1985 Witness
=> nil

クエリ - 1 年間にリリースされた特定のタイトルを持つすべての映画

1992 年にリリースされ,かつ title が A から L までで始まるすべての映画を取得する.ソートキーの属性 title で検索できる. projection_expression は SQL や ActiveRecord の select に等しい.

irb> movies = Movie.query(
irb*   projection_expression: '#yr, title, info.genres, info.actors[0]',
irb*   key_condition_expression: '#yr = :yyyy and title between :letter1 and :letter2',
irb*   expression_attribute_names: { '#yr' => 'year' },
irb*   expression_attribute_values: { ':yyyy' => 1992, ':letter1' => 'A', ':letter2' => 'L' })
=> #<Aws::Record::ItemCollection:0x007fb2811b3830 @search_method=:query, @search_params={:projection_expression=>"#yr, title, info.genres, info.actors[0]", :key_condition_expression=>"#yr = :yyyy and title between :letter1 and :letter2", :expression_attribute_names=>{"#yr"=>"year"}, :expression_attribute_values=>{":yyyy"=>1992, ":letter1"=>"A", ":letter2"=>"L"}, :table_name=>"Movie"}, @model=Movie, @client=#<Aws::DynamoDB::Client>>
irb> movies.each do |movie|
irb*   print "#{movie.year.to_i}: #{movie.title} ... "
irb>   movie.info['genres'].each do |gen|
irb*     print gen + ' '
irb>   end
irb>   print "... #{movie.info['actors'][0]}\n"
irb> end
1992: A Few Good Men ... Crime Drama Mystery Thriller ... Tom Cruise
1992: A League of Their Own ... Comedy Drama Sport ... Tom Hanks
1992: A River Runs Through It ... Drama ... Craig Sheffer
.
.
.
1992: Howards End ... Drama Romance ... Anthony Hopkins
1992: Jennifer Eight ... Crime Drama Mystery Thriller ... Andy Garcia
1992: Juice ... Crime Drama Thriller ... Omar Epps
=> nil

スキャン

1950 年代にリリースされたすべての映画を取得する.パーティションキー year への範囲検索となるためクエリは使えない.スキャンでテーブルの全項目を検索して条件にマッチした結果すべてを取得する.

クエリ・スキャン共通の制限となるが, DynamoDB では検索は対象データが 1 MB を超えない範囲で実行される.検索対象が 1 MB を超える場合は, 1 MB を超えない範囲の検索結果とどこまで検索したかの情報が返却される.すべての検索結果を取得したい場合は,ユーザはどこまで検索したかの情報を元に再度検索を実行する必要がある (ページングのような処理).下記の例で示しているが, Aws:Record を利用している場合は 1 MB を超えた分の検索は自動で実行される.

irb> movies = Movie.scan(
irb*   projection_expression: '#yr, title, info.rating',
irb*   filter_expression: '#yr between :start_yr and :end_yr',
irb*   expression_attribute_names: { '#yr' => 'year' },
irb*   expression_attribute_values: { ':start_yr' => 1950, ':end_yr' => 1959 })
=> #<Aws::Record::ItemCollection:0x007fb27f7b8bd8 @search_method=:scan, @search_params={:projection_expression=>"#yr, title, info.rating", :filter_expression=>"#yr between :start_yr and :end_yr", :expression_attribute_names=>{"#yr"=>"year"}, :expression_attribute_values=>{":start_yr"=>1950, ":end_yr"=>1959}, :table_name=>"Movie"}, @model=Movie, @client=#<Aws::DynamoDB::Client>>
irb> movies.each do |movie|
irb*   puts "#{movie.year.to_i}: #{movie.title} ... #{movie.info['rating'].to_f}"
irb> end
[Aws::DynamoDB::Client 200 0.904206 0 retries] scan(projection_expression:"#yr, title, info.rating",filter_expression:"#yr between :start_yr and :end_yr",expression_attribute_names:{"#yr"=>"year"},expression_attribute_values:{":start_yr"=>{n:"1950"},":end_yr"=>{n:"1959"}},table_name:"Movie")

1952: High Noon ... 8.2
1952: Singin' in the Rain ... 8.4
1952: The Member of the Wedding ... 6.8
.
.
.
1951: Strangers on a Train ... 8.2
1951: The African Queen ... 8.0
1951: The Day the Earth Stood Still ... 7.9
[Aws::DynamoDB::Client 200 0.193829 0 retries] scan(projection_expression:"#yr, title, info.rating",filter_expression:"#yr between :start_yr and :end_yr",expression_attribute_names:{"#yr"=>"year"},expression_attribute_values:{":start_yr"=>{n:"1950"},":end_yr"=>{n:"1959"}},table_name:"Movie",exclusive_start_key:{"title"=>{s:"Iron Man 2"},"year"=>{n:"2010.0"}})

1955: East of Eden ... 8.0
1955: Lady and the Tramp ... 7.4
1955: Les diaboliques ... 8.2
.
.
.
1950: Rashomon ... 8.4
1950: Sunset Blvd. ... 8.6
1950: Tea for Two ... 6.4
=> nil

テーブルを削除する

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/gettingstartedguide/GettingStarted.Ruby.05.html

bundle exec rails g migration drop_movies
# db/migrate/20170310052121_drop_movies.rb

class DropMovies < ActiveRecord::Migration[5.0]
  def change
    migration = Aws::Record::TableMigration.new(Movie)
    migration.delete!
  end
end
irb> Movie.table_exists?
[Aws::DynamoDB::Client 400 0.056389 0 retries] describe_table(table_name:"Movie") Aws::DynamoDB::Errors::ResourceNotFoundException Cannot do operations on a non-existent table

=> false