DynamoDBスクリプトメモ(Python)


本記事について

先日、AWS の Dynamo DB を、Python を用いて操作するコードを書きました。
その際の基本的なスクリプトと、 Dynamo DB 特有の考え方について、備忘としてまとめておきます。
全てを網羅してはいませんが、最低限のことプラスアルファ は記載したつもりです。
見知らぬどなたかのお役に立てれば嬉しいです。

Dynamo DB について

  • AWS が提供する No SQL のデータベース。
  • Amazon DynamoDB(マネージド NoSQL データベース)| AWS
  • Key-Value の形式で高速。
  • JSON も Value として保存できるが、オブジェクト型のDBではない。
  • 結果整合性のため、更新直後の読み込み時には更新内容が間に合っていないこともある(注意!)

Python の環境構築

  • 例によって、AWS の Python 向け SDK である Boto3を利用する。
  • ローカルマシンで開発をする場合は、事前に AWS のアクセスキーを取得し、AWS CLI に認証情報を設定しておく必要がある。
  • Cloud9 を利用する場合も同じく認証情報の設定が必要。注意として、preference 内にある temporary の認証情報トグルをオフにしておかないと、後々厄介なことになる。(機会があったら記事を書きます。)

Dynamo DB の操作には、どのクラスを用いればいいのか?

DynamoDB — Boto3 Docs documentation

最初に...

  • Boto3 には、 Dynamo DB の操作用に、主に Resource classClient class の2つがあり、どちらのクラスを使っても、テーブルへのアイテムの追加や検索は可能。
  • 私も詳しくなく多くを語れないが、以下のサイトを参考にさせてもらうと、「Client class の方が抽象度が低い」とのこと。
    • AWS Chalice で必要な IAM ポリシーが正しく作成されなかったときの話
    • 一点だけ補足すると、上記サイトには Client class でコードを書けば、 Chalice (※Python向けAWSサーバレスフレームワーク)は 必要な IAM ポリシー を自動生成できると書いてあるが、筆者の環境では Client class でも自動生成されなかった。(※ Chalice を利用した際に限った話です。)

Resource class でのインスタンスの作り方

Resource class の方が 実装しやすい印象。

import boto3

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

Client class でのインスタンスの作り方

Client class は 各サービスの API をほぼ網羅していて、「基本的」な印象

import boto3

client = boto3.client('dynamodb')
# テーブル名は、テーブル操作の際に指定する。

スクリプトメモ

  • 個人検証時のメモなので、みなさんの参考にどこまでなるか...

テーブルの定義・内容

  • 駅の情報(都道府県や座標情報)が入っている。
  • プライマリーキーとして、prefectureをパーティションキー、idをソートキーに設定。
  • プライマリーキー(注:パーティションキーとソートキーのこと)も、アトリビュートも、全て文字型。

  • 《注意!》 本記事では、パーティションキーに日本語文字列を使用していますが、あくまで説明上の都合です。

    • 英数字文字列や数字列を使うのが一般的かと思います。

共通部分のコード(Resource class を使用)

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

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

put_item

  • 追加したいアイテムを Item に辞書型で記述する。
  • プライマリーキーの内容は必須。アトリビュートは任意
    • すなわち、プライマリーキーとしてパーティションキーとソートキーの両方を設定している場合は、パーティションキーとソートキーの両方を Item に記述する必要がある。片方しか記述していない場合にはエラーが発生する。
  • Key - Value 型 DB のため、アトリビュートを他のアイテムと揃える必要はない。
    • 下記例の場合、座標情報などを Item に記述していないが問題はない。nullで登録される訳でもない。
response = table.put_item(
    Item = {
        'prefecture':'岩手',
        'id':'5',
        'stationName':'盛岡'
    }
)

print(response)
# {'ResponseMetadata': {'RequestId': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNO', 'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'Server', 'date': 'Mon, 28 Sep 2020 14:24:47 GMT', 'content-type': 'application/x-amz-json-1.0', 'content-length': '2', 'connection': 'keep-alive', 'x-amzn-requestid': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNO', 'x-amz-crc32': '2745614147'}, 'RetryAttempts': 0}}

put_item での Item 更新

  • 同一のプライマリーキーの Item がテーブル上に既存の場合は、Item が更新される。
  • その際、アトリビュートは全て上書きされる。
response = table.put_item(
    Item = {
        'prefecture':'北海道',
        'id':'13',
        'stationName':'姫川(updated)',
        'hoge': 'fuga'
    }
)

print(response)
# {'ResponseMetadata': {'RequestId': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNO', 'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'Server', 'date': 'Mon, 28 Sep 2020 15:02:47 GMT', 'content-type': 'application/x-amz-json-1.0', 'content-length': '2', 'connection': 'keep-alive', 'x-amzn-requestid': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNO', 'x-amz-crc32': '2745614147'}, 'RetryAttempts': 0}}

get_item

  • プライマリーキーとして、パーティションキーのみを設定している場合は、パーティションキーを Key に設定する。
  • プライマリーキーとして、パーティションキーとソートキーの両方を設定している場合は、パーティションキーとソートキーの両方を Key に設定する。(サンプルコードはこのパターン)
    • この場合、パーティションキーのみでの検索はできない。
    • パーティションキーのみでの検索を行いたい場合は、後述のqueryメソッドを利用する
response = table.get_item(
    Key={
        'prefecture':'北海道',
        'id':'1'
    }
)

print(response['Item'])
# {'stationName': '函館', 'prefecture': '北海道', 'id': '1', 'latitude': '41.773709', 'stationId': '1110101', 'longitude': '140.726413'}

query

使い方 例①

ライマリーキーとして、パーティションキーとソートキーの両方を設定している場合でも、queryメソッドであればパーティションキーのみでの検索が可能

response = table.query(
    KeyConditionExpression = Key('prefecture').eq('北海道')
)

print(response['Items'])
# [{'stationName': '函館', 'prefecture': '北海道', 'id': '1', 'latitude': '41.773709', 'stationId': '1110101', 'longitude': '140.726413'}, {'stationName': '赤井川', 'prefecture': '北海道', 'id': '10', 'latitude': '42.003267', 'stationId': '1110110', 'longitude': '140.642678'}, {'stationName': '駒ケ岳', 'prefecture': '北海道', 'id': '11', 'latitude': '42.038809', 'stationId': '1110111', 'longitude': '140.610476'}, {'stationName': '東山', 'prefecture': '北海道', 'id': '12', 'latitude': '42.06172', 'stationId': '1110112', 'longitude': '140.605222'}]

使い方 例②

  • KeyConditionExpression パラメーターにパーティションキーとソートキーの検索条件を記載できる
  • ScanIndexForward パラメーターはデフォルトで True 。 True の時は昇順でのレスポンスとなる。 False にすると降順になる。
    • Limit パラメーターと併用することで、例えば最新の1件のみ取得。等が可能 (ソートキーに時刻を設定している場合)
response = table.query(
    KeyConditionExpression = Key('prefecture').eq('北海道')&Key('id').begins_with('1'),
    ScanIndexForward = False,
    Limit = 2,
)

print(response['Items'])
# [{'stationName': '銚子口', 'prefecture': '北海道', 'id': '16', 'latitude': '42.015471', 'stationId': '1110116', 'longitude': '140.720656'}, {'stationName': '流山温泉', 'prefecture': '北海道', 'id': '15', 'latitude': '42.003483', 'stationId': '1110115', 'longitude': '140.716358'}]

GSI(グローバルセカンダリインデックス)がある場合

query

  • GSIで設定したパーティションキーに対して、上述のget_itemメソッドは使えない。
  • 使えるのはqueryメソッドのみ。
  • 使い方は基本的に同じだが、IndexName パラメーターに GSI のインデックス名を指定する必要がある。
response = table.query(
    IndexName = 'stationName-stationId-index',
    KeyConditionExpression = Key('stationName').eq('流山温泉'),
)

print(response['Items'])
# [{'stationName': '流山温泉', 'prefecture': '北海道', 'id': '15', 'latitude': '42.003483', 'longitude': '140.716358', 'stationId': '1110115'}]

キー以外でフィルターをかけたい場合

  • FilterExpression パラメーターを用いる
response = table.query(
    KeyConditionExpression = Key('prefecture').eq('北海道'),
    FilterExpression = Attr('stationId').begins_with('1110114'),
    ScanIndexForward = False
)

print(response['Items'])
# [{'stationName': '池田園', 'prefecture': '北海道', 'id': '14', 'latitude': '41.990692', 'stationId': '1110114', 'longitude': '140.700333'}]

補足・おすすめサイト