はじめまして。Impulse開発チームの木村です。
今回は、Amazon DynamoDBを、 Apache Cassandraと同じように扱おうとした際に、ハマった点とその解決策を紹介します。
なお、DynamoDBの操作には、AWS SDK for JavaScript (Node.js)を使用しています。
テーブル定義編
テーブルをまとめる機能がない
ハマった点
Cassandraでいうkeyspaceに相当するものがないため、 テーブルをグループでまとめて整理できない。
解決策
テーブル名にプレフィックスを付けて整理する。
テーブル名に.
,_
,-
を使えるため、これらを区切りとして用いる。
参考
複合primary keyに使える属性は、最大で2つ
ハマった点
「3つ以上の属性の複合primary key」を定義できない。
解決策
primary keyとして使用可能なのは、以下のいずれか。
- Hash属性
- Hash属性とRange属性のペア
このため、まずは、2つ以下の属性でprimary keyが定義できる設計を検討してみる。
検討の結果、3つ(以上)の属性の複合primary keyが必要な場合は、たとえば以下のように擬似的に実現する。
- 複合primary keyとして使用したい属性の値を元に、 テーブル内で一意となる値を生成(「idを生成」「値を結合」など)する
- 「生成した値」を代入する属性をテーブルに定義して、primary keyとして扱う
日付・時刻型がない
ハマった点
属性の型に、日付と時刻を表す型がない。
解決策
文字列型または数値型を使って、日付・時刻型を代替する。
- 文字列型の場合
- ISO 8601に従って、日付・時刻を表現
- Range属性として使って、
query
結果を時刻順にソートしたい場合は、 タイムゾーンを統一して、かつ表記揺れをなくす必要がある
- 数値型の場合
- UNIX時間で、日付・時刻を表現
- 閏秒を気にする場合は、また別の方法が必要
- Range属性として使えば、
query
結果は時刻順にソートされる
- UNIX時間で、日付・時刻を表現
参考
NS
/SS
/BS
型は、配列ではない
ハマった点
NS
/SS
/BS
型の値には、同じ要素は1つまでしか入らない。
解決策
NS
/SS
/BS
型はSetを表すため、そのような動作となる。
配列が必要な際は、L
型を使う。
参考
AttributeDefinitions
にkey属性以外を入れてはならない
ハマった点
createTable
でテーブルを作成する際、
(CREATE TABLEにおけるカラム定義の気分で)
AttributeDefinitions
に全ての属性を含めると、
ValidationException
が起きる。
解決策
AttributeDefinitions
には、key属性のみを含める。
(つまり、テーブルの属性定義には、key属性のみが含まれる)
参考
データ取得編
Range属性のみのkey条件指定はできない
ハマった点
(テーブル全体から範囲検索しようとして)
Range属性のみへkey条件指定してquery
を行うと、ValidationException
が起きる。
解決策
key条件指定で有効なのは、
- Hash属性のみへ条件指定
- Hash属性とRange属性の両方へ条件指定
のいずれか。 そのため「Range属性のみへkey条件指定」は行えない。
ただし、「テーブル全体から範囲検索」の実現方法は、例えば以下の方法が考えられる。
scan
を行うFilterExpression
で、Range属性に対して条件範囲を指定scan
が、query
と比べて重い操作であることに注意
- テーブル内のアイテムすべてで、Hash属性の値を同じにする
- key条件として、Hash属性にその値を指定し、Range属性に範囲を指定して、
query
を行う - Hash属性がすべて同じだと、すべてのアイテムが同じpartitionに保存されることに注意
- key条件として、Hash属性にその値を指定し、Range属性に範囲を指定して、
もちろん、「テーブル全体から範囲検索する必要のない」設計の場合は、このハマりは回避できる。
参考
SQL(ライクな)文が使えない
ハマった点
SELECT value1, value2 FROM sample_table WHERE hash_key = 1 AND range_key >= 2
といったSQL文を、query
に指定できない。
解決策
SQL文の内容を、(可能な場合は)query
のパラメータに翻訳する。
たとえば、上記のSQL文では以下のよう。
{ // SELECT "Select": "SPECIFIC_ATTRIBUTES", "ProjectionExpression": "value1, value2", // FROM "TableName": "sample_table", // WHERE "ExpressionAttributeValues": { ":hash": {"N": 1}, ":from": {"N": 2} }, "KeyConditionExpression": "hash_key = :hash AND range_key >= :from" }
Expressionに、数値や文字列を直接書けない
ハマった点
条件指定に使うExpression(KeyConditionExpression
やFilterExpression
など)
に数値や文字列を書くと、ValidationException
が起きる。
ハマる例:
{ "KeyConditionExpression": "hash_key = 1" }
解決策
ExpressionAttributeValues
を使い、値の代わりとなるプレースホルダーを作成して、
Expressionにはそのプレースホルダー名を書く。
ハマらない例:
{ "ExpressionAttributeValues": { // 値のプレースホルダー名は、':'から始める約束 ":hash": {"N": 1}, }, "KeyConditionExpression": "hash_key = :hash" }
参考
Range属性に対して2つの条件を指定できない
ハマった点
KeyConditionExpression
内で、Range属性に条件を2つ指定すると、
ValidationException
が起きる。
ハマる例:
{ "ExpressionAttributeValues": { ":hash": {"N": 1}, ":from": {"N": 100}, ":to" : {"N": 200} }, "KeyConditionExpression": "hash_key = :hash AND range_key >= :from AND range_key <= :to" }
解決策
KeyConditionExpression
での条件指定は、1つのkeyに1つの条件まで。
この例の場合は、BETWEEN
を使うことで制限を回避する。
ハマらない例:
{ "ExpressionAttributeValues": { ":hash": {"N": 1}, ":from": {"N": 100}, ":to" : {"N": 200} }, "KeyConditionExpression": "hash_key = :hash AND range_key BETWEEN :from AND: to" }
Expressionには、含めてはならない予約語がある
ハマった点
Expression(KeyConditionExpression
やProjectionExpression
など)に含まれる属性名が予約語と被ると、ValidationException
が起きる。
ハマる例:
{ "ExpressionAttributeValues": { ":y": {"N": 2015}, }, // yearは予約語 "KeyConditionExpression": "year = :y" }
解決策
ExpressionAttributeNames
を使い、属性名の代わりとなるプレースホルダーを作成して、
Expressionにはそのプレースホルダー名を記述する。
ハマらない例:
{ "ExpressionAttributeNames": { // 属性名のプレースホルダー名は、'#'から始める約束 "#year": "year" }, "ExpressionAttributeValues": { ":y": {"N": 2015}, }, // yearは予約語 "KeyConditionExpression": "#year = :y" }
参考
一度に取得できるテーブル名の数に限界がある
ハマった点
listTables
が返すテーブル名の数は、最大100個。
このため、すべてのテーブル名を一度に取得できないことがある。
解決策
ExclusiveStartTableName
を適宜指定して、listTables
を繰り返し実行することで、
すべてのテーブル名を取得する。
- 最初の実行では、
ExclusiveStartTableName
にnull
を指定 - 2回目以降の実行では、
ExclusiveStartTableName
に 「ひとつ前に実行したlistTables
のレスポンスに含まれるLastEvaluatedTableName
の値」を指定 - テーブル名を最後まで取得した際は、
LastEvaluatedTableName
の値がnull
となる
参考
一度に取得できるアイテムの数に限界がある
ハマった点
query
/scan
が返すアイテムの数は、最大で1MBを超えない数まで。
このため、すべてのアイテムを一度に取得できないことがある。
解決策
ExclusiveStartKey
を適宜指定して、query
/scan
を繰り返し実行することで、
すべてのアイテムを取得する。
- 最初の実行では、
ExclusiveStartKey
にnull
を指定 - 2回目以降の実行では、
ExclusiveStartKey
に「ひとつ前に実行したquery
/scan
のレスポンスに含まれるLastEvaluatedKey
の値」を指定 - テーブル名を最後まで取得した際は、
LastEvaluatedKey
の値がnull
となる
ちなみに「AWS SDK for Java ドキュメント API」のquery
/scan
では、
この繰り返しをAPIが裏で行ってくれる。
参考
データ追加・更新編
空文字列を代入できない
ハマった点
S
型の属性に空文字列を代入しようとすると、ValidationException
が起きる。
解決策
DynamoDBの制限のため、空文字列は代入できない。
空文字列を代入せずに済む仕様にする必要がある。
参考
batchWriteItem
で、一度にputできるアイテム数に限界がある
ハマった点
batchWriteItem
が一度に受け付けるRequest数は、最大25個。
26個以上のアイテムは一度にputできない。
解決策
アイテムは、25個以下ずつputする。
参考
batchWriteItem
で、同じアイテムへのRequestは重複不可
ハマった点
batchWriteItem
の実行時、
同じアイテムへのRequestがRequestItems
内で重複すると、
ValidationException
が起きる。
同じアイテムかどうかは、同じkey属性値かどうかで決まる。
以下の、どのパターンでも起きる。
PutRequest
間で重複DeleteRequest
間で重複PutRequest
とDeleteRequest
間で重複
解決策
重複しないようにする。
たとえば、 「オブジェクトの配列から複数のRequestを自動生成する場合」などに注意。 Requestの生成前か生成後に、重複排除のためのfilteringを行うなどして対処する。
参考
batchWriteItem
成功時、全Requestが成功したとは限らない
ハマった点
batchWriteItem
の成功は、すべてのRequestの成功を保証しない。
解決策
失敗したRequestを特定して、ケアする必要がある。
batchWriteItem
のレスポンスに含まれるUnprocessedItems
に、
失敗したRequestがリストアップされるので、
これを用いて、batchWriteItem
を再び実行する。
参考
おわりに
開発中にDynamoDBでハマった点を紹介しました。
新しいものに触った時はハマるのが常ですが、 ドキュメントとソースを見ながら、ググりながら、 落ち着いて理解/実験していけば、何とかなるものです(と信じたい)。
ハマりすぎて落ち着けなくなったら、帰りましょう。
ブレインズテクノロジーでは「共に成長できる仲間」を募集中です
採用ページはこちら