boto3のclientでDynamoDBテーブルを利用してみる(scan, put_item, get_item, update_item, delete_item)

boto3のclientでDynamoDBを利用してみます。型情報が大変です。
2023.01.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

boto3でDynamoDBを利用するとき、リソース(resource)を利用していました。 しかし、次のニュースが飛び込んできました。

おそらく、最も影響を受けるのは、DynamoDBに対するアクセスだと思います。 そこで、本記事では、DynamoDBに対するアクセスをresourceではなくclientで試してみました。

2023/1/23追記:表現が変わりました。

resourceは無くなりませんが、新機能の追加がされないようです。

Resources

おすすめの方

  • boto3のclientでDynamoDBテーブルを利用したい方

環境

項目 バージョン
Python 3.9.9
boto3 1.26.52
botocore 1.29.52

boto3のDynamoDBで非推奨になるであろう内容

ドキュメントの下記が該当すると思われます。

簡単に説明すると、今までお手軽に使っていた次のコードたちが非推奨になります。

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('name')

table.foo()

実験用のDynamoDBテーブルを用意する

DynamoDBテーブルを作成する

適当に作成します。

テスト用のDynamoDBテーブルを作成する

boto3のclientでDynamoDBを利用するサンプル

サンプルコード

sample.py

import boto3
from boto3.dynamodb.conditions import Attr, Key
from datetime import datetime, timezone

TABLE_NAME = 'boto3-test'

dynamodb = boto3.client('dynamodb')

# これはできない
# table = dynamodb.Table('boto3-test')

def main():
    print('--- scan ---')
    scan()

    print('--- put ---')
    put()

    print('--- get ---')
    get()

    print('--- update ---')
    update()

    print('--- scan ---')
    scan()

    print('--- delete ---')
    delete()

    print('--- scan ---')
    scan()


def scan():
    options = {
        'TableName': TABLE_NAME,
        'ProjectionExpression': '#todoId, #title',
        'ExpressionAttributeNames': {
            '#todoId': 'todoId',
            '#title': 'title',
        },
        'Limit': 1,     # loopの実験用
    }

    ret = []
    while True:
        res = dynamodb.scan(**options)
        ret += res.get('Items', [])
        if 'LastEvaluatedKey' not in res:
            break
        options['ExclusiveStartKey'] = res['LastEvaluatedKey']

    print(len(ret))
    print(ret)



def put():
    now = int(datetime.now(timezone.utc).timestamp() * 1000)
    options = {
        'TableName': TABLE_NAME,
        'Item': {
            'todoId': {'S': 't0001'},
            'title': {'S': 'this is title'},
            'status': {'S': 'unstarted'},
            'createdAt': {'N': str(now)},
            'binary': {'B': 'pen'.encode()},
            'any string set': {'SS': ['Tag Name1', 'Tag Name2', 'Tag Name3']},
            'any number set': {'NS': ['111','222','333']},
            'any binary set': {'BS': ['aaa'.encode(), 'bbb'.encode(), 'ccc'.encode()]},
            'any map': {'M': {
                'map-name1': {'S': 'map-value1'},
                'map-name2': {'N': '111'},
            }},
            'any list': {'L': [
                {'S': 'list-value1'},
                {'N': '222'},
            ]},
            'any null': {'NULL': True},
            'any bool': {'BOOL': False},
        },
        # 'ConditionExpression': 'attribute_not_exists(todoId)',
    }
    dynamodb.put_item(**options)

    options['Item']['todoId']['S'] = 't0002'
    dynamodb.put_item(**options)


def get():
    options = {
        'TableName': TABLE_NAME,
        'Key': {
            'todoId': {'S': 't0001'},
        }
    }
    ret = dynamodb.get_item(**options)

    print(ret['Item'])


def update():
    now = int(datetime.now(timezone.utc).timestamp() * 1000)
    options = {
        'TableName': TABLE_NAME,
        'Key': {
            'todoId': {'S': 't0001'},
        },
        'ConditionExpression': '#status = :unstarted',
        'UpdateExpression': 'set #updatedAt = :updatedAt, #status = :running, #title = :title',
        'ExpressionAttributeNames': {
            '#updatedAt': 'updatedAt',
            '#status': 'status',
            '#title': 'title',
        },
        'ExpressionAttributeValues': {
            ':updatedAt': {'N': str(now)},
            ':unstarted': {'S': 'unstarted'},
            ':running': {'S': 'running'},
            ':title': {'S': 'This is an apple.'},
        }
        # 'ConditionExpression': 'attribute_not_exists(todoId)',
    }
    dynamodb.update_item(**options)


def delete():
    options = {
        'TableName': TABLE_NAME,
        'Key': {
            'todoId': {'S': 't0001'},
        }
    }
    dynamodb.delete_item(**options)


if __name__ == '__main__':
    main()

実行結果

--- scan ---
0
[]
--- put ---
--- get ---
{'any list': {'L': [{'S': 'list-value1'}, {'N': '222'}]}, 'todoId': {'S': 't0001'}, 'any number set': {'NS': ['111', '222', '333']}, 'status': {'S': 'unstarted'}, 'any null': {'NULL': True}, 'createdAt': {'N': '1674124101857'}, 'any map': {'M': {'map-name1': {'S': 'map-value1'}, 'map-name2': {'N': '111'}}}, 'any string set': {'SS': ['Tag Name1', 'Tag Name2', 'Tag Name3']}, 'any bool': {'BOOL': False}, 'any binary set': {'BS': [b'aaa', b'bbb', b'ccc']}, 'binary': {'B': b'pen'}, 'title': {'S': 'this is title'}}
--- update ---
--- scan ---
2
[{'todoId': {'S': 't0001'}, 'title': {'S': 'This is an apple.'}}, {'todoId': {'S': 't0002'}, 'title': {'S': 'this is title'}}]
--- delete ---
--- scan ---
1
[{'todoId': {'S': 't0002'}, 'title': {'S': 'this is title'}}]

DynamoDBにputされたItem

実行結果後の情報です。

{
  "todoId": {
    "S": "t0002"
  },
  "any binary set": {
    "BS": [
      "YWFh",
      "YmJi",
      "Y2Nj"
    ]
  },
  "any bool": {
    "BOOL": false
  },
  "any list": {
    "L": [
      {
        "S": "list-value1"
      },
      {
        "N": "222"
      }
    ]
  },
  "any map": {
    "M": {
      "map-name1": {
        "S": "map-value1"
      },
      "map-name2": {
        "N": "111"
      }
    }
  },
  "any null": {
    "NULL": true
  },
  "any number set": {
    "NS": [
      "111",
      "222",
      "333"
    ]
  },
  "any string set": {
    "SS": [
      "Tag Name1",
      "Tag Name2",
      "Tag Name3"
    ]
  },
  "binary": {
    "B": "cGVu"
  },
  "createdAt": {
    "N": "1674124101857"
  },
  "status": {
    "S": "unstarted"
  },
  "title": {
    "S": "this is title"
  }
}

型変換の代替策

DynamoDBテーブルの型情報が必要なのがとても手間です。 その代替策として、とりあえず、3つありそうです。

boto3のTypeSerializerとTypeDeserializerを利用する

boto3で完結しており、他ライブラリが不要なので、これが良さそうですね。

型情報をいい感じにしてくれるライブラリを使う

「通常のJSON」と「DynamoDB用のJSON」を変換してくれるライブラリです。

ただし、メンテナンスはされていなさそうですので、万全を期すなら、forkして利用すると安心ですね。

PynamoDBを使う

PythonでDynamoDBを扱うライブラリです。

PynamoDBはboto3本体に依存しておらず、botocoreを利用しているため、今回のようなboto3の変更には影響を受けないと思われます。

$ pip install pynamodb
Collecting pynamodb
  Downloading pynamodb-5.3.4-py3-none-any.whl (58 kB)
     |████████████████████████████████| 58 kB 6.6 MB/s 
Collecting botocore>=1.12.54
  Using cached botocore-1.29.52-py3-none-any.whl (10.3 MB)
Collecting urllib3<1.27,>=1.25.4
  Using cached urllib3-1.26.14-py2.py3-none-any.whl (140 kB)
Collecting python-dateutil<3.0.0,>=2.1
  Using cached python_dateutil-2.8.2-py2.py3-none-any.whl (247 kB)
Collecting jmespath<2.0.0,>=0.7.1
  Using cached jmespath-1.0.1-py3-none-any.whl (20 kB)
Collecting six>=1.5
  Using cached six-1.16.0-py2.py3-none-any.whl (11 kB)
Installing collected packages: six, urllib3, python-dateutil, jmespath, botocore, pynamodb
Successfully installed botocore-1.29.52 jmespath-1.0.1 pynamodb-5.3.4 python-dateutil-2.8.2 six-1.16.0 urllib3-1.26.14

さいごに

Pythonでboto3を利用している場合には、かなり大きく影響のあるアップデート通知です。 今すぐの対応は不要ですが、今後どうしていくかの作戦は決めておくほうが良さそうですね。

参考