DynamoDBを初めて触ってハマったこと

AWSのDynamoDBを初めて触ったときの感想というか、つまづいた点を記録しておきます。

その1

CloudFormationでDynamoDBのテーブルを作成しようとしたら

The number of attributes in key schema must match the number of attributesdefined in attribute definitions.

というエラーになりました。 AttributeDefinitions にカラムを全部書くものと思い込んでいたので、すべてのカラムをキーに指定しないといけない、、、、え?そんななんてわけはないよね、となり混乱しました。

AttributeDefinitions に書くのはパーティションキーとソートキーのみでよく、ほかは書いてはいけないというのが正解でした。テーブル作成時ではなく、レコード挿入時に自由にカラムを増やせます。

その2

その1と関連しますが、

CloudFormationでDyanomoDBのテーブルを作成しようと、データ型の指定方法を調べると、文字列と数字とバイナリの3種類しかありません。

AWS::DynamoDB::Table AttributeDefinition - AWS CloudFormation

CloudFormationから作成するときはこの3種類のデータ型しか使えないのかと混乱しました。しかし、そんなことはありません。これはプライマリキーとソートキーに使えるデータ型がこの3種類というだけです。プライマリキーとソートキー以外のカラムはCloudFormationに書かなくてよいので、レコード挿入時に自由なデータ型を使えばよいようです。

サポートされているデータの種類 - Amazon DynamoDB

その3

CloudFormationから作成するときに BillingModeProvisionedThroughput という2つの項目があります。どちらも必須ではないのですが、 BillingMode を省略すると ProvisionedThroughput が必須に変わります。したがって少なくともどちらかを書かないといけません。

BillingMode は以下の2種類から選択できます。

  • オンデマンド(従量課金) PAY_PER_REQUEST
  • プロビジョニング PROVISIONED (デフォルト)

CloudFormationから作成するときに BillingMode を省略すると、 PROVISIONED とみなされます。 PROVISIONED の場合は ProvisionedThroughput が必須に変わるのです。

開発環境で自分しか触らないのであればオンデマンドのほうが安上がりのようです。

BillingMode は必須ではないけど常に書くことにしておいたほうがよさそうです。

CloudFormationのテンプレートのサンプル

AWSTemplateFormatVersion: '2010-09-09'

Resources:
  SampleTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: sample
      AttributeDefinitions:
        - AttributeName: UserId
          AttributeType: S
        - AttributeName: TransactionId
          AttributeType: S
      KeySchema:
        - AttributeName: UserId
          KeyType: HASH # パーティションキー
        - AttributeName: TransactionId
          KeyType: RANGE # ソートキー
      TimeToLiveSpecification:
        AttributeName: Expiration
        Enabled: true
      BillingMode: PAY_PER_REQUEST

DynamoDBにPythonからアクセスするサンプルコード

Pythonのboto3を使ったサンプルコードです。

session = boto3.session.Session()
dynamodb_resource = session.resource("dynamodb")
table = dynamodb_resource.Table("sampletable")

# 全レコード取得
# 全レコードがrecordsに入る
records = []
response = table.scan()
while True:
    for item in response["Items"]:
        records.append(item)
    if not "LastEvaluatedKey" in response:
        break
    key = response["LastEvaluatedKey"]
    response = table.scan(ExclusiveStartKey = key)

# 1レコード取得
response = table.get_item(
    Key = {
        "UserId": userId,
        "TransactionId": transId,
    },
)
if not "Item" in response:
    "Not Found!"
record = response["Item"]

# レコード挿入または上書き
table.put_item(
    Item = {
        "UserId": userId,
        "TransactionId": transId,
        "Foo": foo,
        "Bar": bar,
    },
)