ブログはじめました(仮)

飽きずに続けばタイトル変更します。見習いエンジニアですが、備忘録的に色々と書いてみたいと思います。

Amazon DynamoDBをrubyで触ってみる 第2弾〜update_item〜

前回に引き続きDynamoDBrubyでゴニョゴニョしてみました。

今回は、ドキュメントを読んで(個人的に)わかりづらかったupdate_itemについてです。

update_itemというのは、その名の通り「すでに登録してあるアイテムに変更を加える操作」です。
オプションに、PUT・ADD・DELETEというのを指定するのですが、その使い分けをまとめてみました。

まず、アトリビュート(カラムに相当)の型の種類を理解しておく必要があります。
全部で6種類あります。

タイプ 説明
N 数字
S 文字列
B バイナリ
NS 数字の配列
SS 文字列の配列
BS バイナリの配列

続いて、PUT,ADD,DELETEの違いについてです。

PUT 問答無用でそのアトリビュートを置き換え。存在しなければアトリビュートごと作る
ADD 型がNのときは、足し算。型がセット(NS,SS,BS)のときは、配列に追加
DELETE 型がN,S,Bのときは、そのアトリビュートを削除。セットのときは、その要素のみを削除

となります。


それでは実際に試してみたいと思います。

まずはテーブルを作ってアイテムを保存します。

require 'rubygems'
require 'aws-sdk'
require './init.rb'

dynamoClient = AWS::DynamoDB::Client.new({
  :dynamo_db_endpoint => 'dynamodb.ap-northeast-1.amazonaws.com',
  :access_key_id => ACCESS,
  :secret_access_key => SECRET})

dynamoClient.create_table(
  :table_name => TABLE,
  :key_schema => {:hash_key_element => {:attribute_name => "id",
                                        :attribute_type => "N"}},
  :provisioned_throughput => {:read_capacity_units => 2,
                              :write_capacity_units => 2}
)

sleep 20

dynamoClient.put_item(
  :table_name => TABLE,
  :item => {
    "id" => {:n => "1"},
    "put" => {:n => "1"},
    "add" => {:n => "1"},
    "delete" => {:n => "1"}
  }
)

dynamoClient.put_item(
  :table_name => TABLE,
  :item => {
    "id" => {:n => "2"},
    "put" => {:ns => ["1","2","3"]},
    "add" => {:ns => ["1","2","3"]},
    "delete" => {:ns => ["1","2","3"]}
  }
)

続いて、アップデートを実行します。

require 'rubygems'
require 'aws-sdk'
require './init.rb'

dynamoClient = AWS::DynamoDB::Client.new({
  :dynamo_db_endpoint => 'dynamodb.ap-northeast-1.amazonaws.com',
  :access_key_id => ACCESS,
  :secret_access_key => SECRET})

init = dynamoClient.get_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "1"}
  }
)

p init

#PUT
dynamoClient.update_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "1"}
  },
  :attribute_updates => {
    "put" => {
      :value => {:n => "100"},
      :action => "PUT"
    }
  },
)

put = dynamoClient.get_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "1"}
  }
)

p put

#ADD
dynamoClient.update_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "1"}
  },
  :attribute_updates => {
    "add" => {
      :value => {:n => "100"},
      :action => "ADD"
    }
  },
)

add = dynamoClient.get_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "1"}
  }
)

p add

#DELETE
dynamoClient.update_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "1"}
  },
  :attribute_updates => {
    "delete" => {
      :value => {:n => "100"},
      :action => "DELETE"
    }
  },
)

delete = dynamoClient.get_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "1"}
  }
)

p delete
{"Item"=>{"id"=>{"N"=>"1"}, "add"=>{"N"=>"1"}, "put"=>{"N"=>"1"}, "delete"=>{"N"=>"1"}}, "ConsumedCapacityUnits"=>0.5}
{"Item"=>{"id"=>{"N"=>"1"}, "add"=>{"N"=>"1"}, "put"=>{"N"=>"100"}, "delete"=>{"N"=>"1"}}, "ConsumedCapacityUnits"=>0.5}
{"Item"=>{"id"=>{"N"=>"1"}, "add"=>{"N"=>"101"}, "put"=>{"N"=>"100"}, "delete"=>{"N"=>"1"}}, "ConsumedCapacityUnits"=>0.5}
/Users/tetsuyam/.rvm/gems/ruby-1.9.3-p385/gems/aws-sdk-1.10.0/lib/aws/core/client.rb:360:in `return_or_raise': One or more parameter values were invalid: Action DELETE is not supported for the type N (AWS::DynamoDB::Errors::ValidationException)
	from /Users/tetsuyam/.rvm/gems/ruby-1.9.3-p385/gems/aws-sdk-1.10.0/lib/aws/core/client.rb:461:in `client_request'
	from (eval):3:in `update_item'
	from updateItem1-2.rb:66:in `<main>'

PUTのときは、1→100。ADDのときは、1→101となることが確認できます。
DELETEだとエラーがでます。
タイプがNのときのDELETEでは、アトリビュートの値を指定する必要がないようです。(むしろ指定したらダメ)

#DELETE
dynamoClient.update_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "1"}
  },
  :attribute_updates => {
    "delete" => {
      :value => {:n => "100"},
      :action => "DELETE"
    }
  },
)

とすると

{"Item"=>{"add"=>{"N"=>"101"}, "id"=>{"N"=>"1"}, "put"=>{"N"=>"100"}}, "ConsumedCapacityUnits"=>0.5}

となり、うまくいきました。



次に、SET型の場合を試したいと思います。

require 'rubygems'
require 'aws-sdk'
require './init.rb'

dynamoClient = AWS::DynamoDB::Client.new({
  :dynamo_db_endpoint => 'dynamodb.ap-northeast-1.amazonaws.com',
  :access_key_id => ACCESS,
  :secret_access_key => SECRET})

init = dynamoClient.get_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "2"}
  }
)

p init

#PUT
dynamoClient.update_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "2"}
  },
  :attribute_updates => {
    "put" => {
      :value => {:ns => ["100"]},
      :action => "PUT"
    }
  },
)

put = dynamoClient.get_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "2"}
  }
)

p put

#ADD
dynamoClient.update_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "2"}
  },
  :attribute_updates => {
    "add" => {
      :value => {:ns => ["100"]},
      :action => "ADD"
    }
  },
)

add = dynamoClient.get_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "2"}
  }
)

p add

#DELETE
dynamoClient.update_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "2"}
  },
  :attribute_updates => {
    "delete" => {
      :value => {:ns => ["1"]},
      :action => "DELETE"
    }
  },
)

delete = dynamoClient.get_item(
  :table_name => TABLE,
  :key => {
    :hash_key_element => {:n => "2"}
  }
)

p delete
|ruby|<


>||
{"Item"=>{"id"=>{"N"=>"2"}, "add"=>{"NS"=>["3", "100", "2", "1"]}, "put"=>{"N"=>"100"}, "delete"=>{"NS"=>["3", "2", "1"]}}, "ConsumedCapacityUnits"=>0.5}
{"Item"=>{"id"=>{"N"=>"2"}, "add"=>{"NS"=>["3", "2", "1"]}, "put"=>{"NS"=>["100"]}, "delete"=>{"NS"=>["3", "2","1"]}}, "ConsumedCapacityUnits"=>0.5}
{"Item"=>{"id"=>{"N"=>"2"}, "add"=>{"NS"=>["3", "100", "2", "1"]}, "put"=>{"NS"=>["100"]}, "delete"=>{"NS"=>["3", "2", "1!]}}, "ConsumedCapacityUnits"=>0.5}
{"Item"=>{"id"=>{"N"=>"2"}, "add"=>{"NS"=>["3", "100", "2", "1"]}, "put"=>{"NS"=>["100"]}, "delete"=>{"NS"=>["3", "2"]}}, "ConsumedCapacityUnits"=>0.5}

PUTでは置き換え、ADDでは追加、DELETEでは削除されていることがわかります。


ADDで足し算できるのは、カウントするときとかに便利そうですね。

Amazon DynamoDBをrubyで触ってみる

初めてのブログですが、AWSのDynamoDBに新機能が加わったそうなので、
DynamoDBrubyでゴニョゴニョしてみました。
(*ただし新機能は使っていませんww)

Amazonさん曰く、

DynamoDB を使用すると、可用性の高い分散データベースクラスターの運用とスケーリングという管理上の負担を軽減しつつ、使用した分だけ低額の料金を支払うことができます。

というサービスです。

テーブルを作成するときに、上限値となる書込/読込のユニット数を指定します。

その値によって値段が決まるという仕組みです。


つまり、

(テーブルの性能×時間)+(保存されたデータの量×時間)+(データ転送量)でお値段が決まります。

ちなみに僕は、「使用した分だけ=出し入れした回数に応じて課金」と勘違いしたせいで10万円を超える請求が来ました\(^o^)/
テーブルを作っていれば使っていなくても課金されるので、皆さんご注意ください(´・ω・`)
(EC2だって起動してれば課金されるし、少し考えれば分かることでした)


テーブルの性能の定義の仕方ですが、「ユニット」という単位を用います。

詳細な説明は省きますが、 (Amazon DynamoDB 料金表 | アマゾン ウェブ サービス(AWS 日本語)

書き込みに必要な容量のユニット数 = 1 秒あたりの項目書き込み回数 x 項目のサイズ(KB に切り上げ)

読み込みに必要な容量のユニット数¹ = 1 秒あたりの項目読み込み回数 x 項目のサイズ(KB に切り上げ)

となっています。
つまり、書込ユニット=10と設定してテーブルを作ると、
1KB以下のアイテムであれば秒間10回の書込が許されるテーブルが作成できるということです。


それでは実際に試してみたいと思います。

まずはテーブルを作ってみます。

require 'rubygems'
require 'aws-sdk'

dynamoClient = AWS::DynamoDB::Client.new({
  :dynamo_db_endpoint => 'dynamodb.ap-northeast-1.amazonaws.com',
  :access_key_id => "*******",
  :secret_access_key => "******"})

table = dynamoClient.create_table(
  :table_name => "test",
  :key_schema => {:hash_key_element => {:attribute_name => "id",
                                        :attribute_type => "N"}},
  :provisioned_throughput => {:read_capacity_units => 2,
                              :write_capacity_units => 2}
)
p table

"東京リージョン"に、"test"というテーブルを"書/読 いずれも2ユニットを上限"として作りました。

{"TableDescription"=>{"TableName"=>"test", "KeySchema"=>{"HashKeyElement"=>{"AttributeName"=>"id", "AttributeType"=>"N"}}, "TableStatus"=>"CREATING", "CreationDateTime"=>2013-05-25 12:40:02 +0900, "ProvisionedThroughput"=>{"NumberOfDecreasesToday"=>0, "ReadCapacityUnits"=>2, "WriteCapacityUnits"=>2}, "TableSizeBytes"=>0, "ItemCount"=>0}}

続いて、アイテムをputしてgetしてみます。

require 'rubygems'
require 'aws-sdk'

dynamoClient = AWS::DynamoDB::Client.new({
  :dynamo_db_endpoint => 'dynamodb.ap-northeast-1.amazonaws.com',
  :access_key_id => "********",
  :secret_access_key => "***********"})

result = dynamoClient.put_item(
  :table_name => "test",
  :item => {
    "id" => {:n => "1"},
    "name" => {:s => "User1"},
    "body" => {:s => "This is User1"}
  },
  :return_values => "ALL_OLD"
)
p result

result = dynamoClient.get_item(
  :table_name => "test",
  :key => {
    :hash_key_element => {:n => "1"}
  },
  :attributes_to_get => ["name","body"],
  :consistent_read => true
)
p  result
{"ConsumedCapacityUnits"=>1.0}
{"Item"=>{"name"=>{"S"=>"User1"}, "body"=>{"S"=>"This is User1"}}, "ConsumedCapacityUnits"=>1.0}

問題なくできました。



最後に、テーブル作成時の上限を超えた場合はどうなるのかを試してみます。
今回のテーブルでは、書込は秒間2回しかできないはずなので、秒間10回ぐらい叩いてみたいと思います。

require 'rubygems'
require 'aws-sdk'

dynamoClient = AWS::DynamoDB::Client.new({
  :dynamo_db_endpoint => 'dynamodb.ap-northeast-1.amazonaws.com',
  :access_key_id => "********",
  :secret_access_key => "***********"})

start = Time.now
10.times{|i|
  result = dynamoClient.put_item(
    :table_name => "test",
    :item => {
      "id" => {:n => i.to_s},
      "name" => {:s => "User"+i.to_s},
      "body" => {:s => "This is User"+i.to_s}
    },
    :return_values => "ALL_OLD"
  )
  p result
}
fin = Time.now

p fin - start
{"ConsumedCapacityUnits"=>1.0}
{"Attributes"=>{"name"=>{"S"=>"User1"}, "id"=>{"N"=>"1"}, "body"=>{"S"=>"This is User1"}}, "ConsumedCapacityUnits"=>1.0}
{"ConsumedCapacityUnits"=>1.0}
{"ConsumedCapacityUnits"=>1.0}
{"ConsumedCapacityUnits"=>1.0}
{"ConsumedCapacityUnits"=>1.0}
{"ConsumedCapacityUnits"=>1.0}
{"ConsumedCapacityUnits"=>1.0}
{"ConsumedCapacityUnits"=>1.0}
{"ConsumedCapacityUnits"=>1.0}
1.265363

なんという事でしょう!!
なぜか、すべて処理されてしまう。。。。
ユーザー的にはありがたいことですが、なんとなく気持ち悪い。


Amazonさんが太っ腹すぎるってことなんでしょうか?
誰かご存知の方いれば教えて下さい。


少し調べてみたところ
デフォルトの設定ではDynamoクライアントが、
「ちょwwそんな大量のリクエスト無理。上限設定値増やして!」
というエラーが返ってくると自動的にリクエストを再送するようになってるらしい。
それをオフにするには、

dynamoClient = AWS::DynamoDB::Client.new({
  :dynamo_db_endpoint => 'dynamodb.ap-northeast-1.amazonaws.com',
  <b>:dynamo_db_retry_throughput_errors => false,</b>
  :access_key_id => "********",
  :secret_access_key => "***********"})

とすれば良いとのことなので、再度試してみた。(今度は1000回ループ)
するとあるタイミングで、

/Users/tetsuyam/.rvm/gems/ruby-1.9.3-p385/gems/aws-sdk-1.10.0/lib/aws/core/client.rb:360:in `return_or_raise': The level of configured provisioned throughput for the table was exceeded. Consider increasing your provisioning level with the UpdateTable API (AWS::DynamoDB::Errors::ProvisionedThroughputExceededException)
	from /Users/tetsuyam/.rvm/gems/ruby-1.9.3-p385/gems/aws-sdk-1.10.0/lib/aws/core/client.rb:461:in `client_request'
	from (eval):3:in `put_item'
	from rep_put.rb:13:in `block in <main>'
	from rep_put.rb:12:in `times'
	from rep_put.rb:12:in `<main>'

というエラーが出るのですが、これも出るときと出ないときがあります。

そもそも、仕様上は秒間3回目の時点で出るのが正しいと思うのですが、どうなんでしょう?




メモとして、put,get以外のサンプルコードも載せときます。

#ユニット上限設定値の変更
table = dynamoClient.update_table(
  :table_name => "test",
  :provisioned_throughput => {:read_capacity_units => "100",
                              :write_capacity_units => "100"}
)

#アイテムのアップデート
result = dynamoClient.update_item(
  :table_name => "test",
  :key => {
    :hash_key_element => {:n => "1"}
  },
  :attribute_updates => {
    "comment" => {
      :value => {:s => "hello"},
      :action => "PUT"
    }
  },
  :return_values => "ALL_OLD"
)

#テーブルの削除
table = dynamoClient.delete_table(
  :table_name => "test"
)