Technology Topics by Brains

ブレインズテクノロジーの研究開発機関「未来工場」で働くエンジニアが、先端オープン技術、機械学習×データ分析(異常検知、予兆検知)に関する取組みをご紹介します。

ここにハマった!DynamoDB

はじめまして。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が必要な場合は、たとえば以下のように擬似的に実現する。

  1. 複合primary keyとして使用したい属性の値を元に、 テーブル内で一意となる値を生成(「idを生成」「値を結合」など)する
  2. 「生成した値」を代入する属性をテーブルに定義して、primary keyとして扱う

日付・時刻型がない

ハマった点

属性の型に、日付と時刻を表す型がない。

解決策

文字列型または数値型を使って、日付・時刻型を代替する。

  • 文字列型の場合
    • ISO 8601に従って、日付・時刻を表現
    • Range属性として使って、query結果を時刻順にソートしたい場合は、 タイムゾーンを統一して、かつ表記揺れをなくす必要がある
  • 数値型の場合
    • UNIX時間で、日付・時刻を表現
      • 閏秒を気にする場合は、また別の方法が必要
    • Range属性として使えば、query結果は時刻順にソートされる

参考


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に保存されることに注意

もちろん、「テーブル全体から範囲検索する必要のない」設計の場合は、このハマりは回避できる。

参考


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(KeyConditionExpressionFilterExpressionなど) に数値や文字列を書くと、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(KeyConditionExpressionProjectionExpressionなど)に含まれる属性名が予約語と被ると、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を繰り返し実行することで、 すべてのテーブル名を取得する。

  • 最初の実行では、ExclusiveStartTableNamenullを指定
  • 2回目以降の実行では、ExclusiveStartTableNameに 「ひとつ前に実行したlistTablesのレスポンスに含まれるLastEvaluatedTableNameの値」を指定
  • テーブル名を最後まで取得した際は、LastEvaluatedTableNameの値がnullとなる

参考


一度に取得できるアイテムの数に限界がある

ハマった点

query/scanが返すアイテムの数は、最大で1MBを超えない数まで。

このため、すべてのアイテムを一度に取得できないことがある。

解決策

ExclusiveStartKeyを適宜指定して、query/scanを繰り返し実行することで、 すべてのアイテムを取得する。

  • 最初の実行では、ExclusiveStartKeynullを指定
  • 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間で重複
  • PutRequestDeleteRequest間で重複

解決策

重複しないようにする。

たとえば、 「オブジェクトの配列から複数のRequestを自動生成する場合」などに注意。 Requestの生成前か生成後に、重複排除のためのfilteringを行うなどして対処する。

参考


batchWriteItem成功時、全Requestが成功したとは限らない

ハマった点

batchWriteItemの成功は、すべてのRequestの成功を保証しない。

解決策

失敗したRequestを特定して、ケアする必要がある。

batchWriteItemのレスポンスに含まれるUnprocessedItemsに、 失敗したRequestがリストアップされるので、 これを用いて、batchWriteItemを再び実行する。

参考


おわりに

開発中にDynamoDBでハマった点を紹介しました。

新しいものに触った時はハマるのが常ですが、 ドキュメントとソースを見ながら、ググりながら、 落ち着いて理解/実験していけば、何とかなるものです(と信じたい)。

ハマりすぎて落ち着けなくなったら、帰りましょう。