Cassandra を別の技術 (フルマネージドサービス) で置き換えるべく模索中.
資料
チュートリアル
ベストプラクティス
- DynamoDB のベストプラクティス - Amazon DynamoDB
- DynamoDB ベストプラクティス - Qiita
- AWS Solutions Architect ブログ: 【AWS Database Blog】DynamoDB におけるパーティションキー設計の手引き
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
サンプルデータをロードする
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>>
項目を作成,読み込み,更新,削除する
項目を作成する.プライマリキーを指定する.
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
テーブルを削除する
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