[Python][Tips] DynamoDBテーブルでもアトミックカウンターを更新したい

DynamoDB

こんにちは。こむろ@札幌です。北の大地が本気を出してきました。本日(2016/11/09大統領選挙戦)は最高気温0℃です。寒すぎる。 最近は専ら運用サイドでAWSのマネジメントコンソールとお友達です。

アトミックカウンタの更新をしたい

今回はDynamoDBのテーブルの項目をPythonで操作する際に色々と試行錯誤したので、そちらの記録です。

あるDynamoDBのテーブルに定義されているアトミックカウンタの項目がありました。こちらをインクリメントする単純なスクリプトが欲しかったのですが、AWS公式ドキュメントを参照して実装してみるとうまく行きませんでした。

今回は色々悩んだ挙句、弊社屈指の蛇使いであるサーモン横山くんに、色々と教えてもらいつつ、七転八倒しながらスクリプトを作成しました。

環境

自分の環境は以下になります。

  • OS: Mac OSX 10.10.5
  • Python: 3.4.3

アトミックカウンター

アトミックカウンターについてはこちらを参照ください。以下のような条件に利用することができます

  • アトミックカウンタとして定義している項目である
  • 項目名の先頭文字が記号から始まってる(_ など)

特に 2つ目の条件 が当てはまる場合は、以下のスクリプトで当該項目の更新が可能です。まずは正解のスクリプトを確認してみます。

from __future__ import print_function # Python 2/3 compatibility
import boto3
from botocore.exceptions import ClientError
from boto3.session import Session
import json
import time
import decimal

# Helper class to convert a DynamoDB item to JSON.
class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            if o % 1 > 0:
                return float(o)
            else:
                return int(o)
        return super(DecimalEncoder, self).default(o)

# versionを強制的に+1する
def update_version(hashId):
    response = user.update_item(
        Key= {
            'hashid': hashId
            },
        UpdateExpression="ADD #name :increment",
        ExpressionAttributeNames={
            '#name':'_version'
            },
        ExpressionAttributeValues={
            ":increment": decimal.Decimal(1)
            },
        ReturnValues="UPDATED_NEW"
    )
    print('%s, update_version: %d' % (hashId, response.get('Attributes', {}).get('_version', -99)))

# setting profile
profile = 'my_profile'
session = Session(profile_name=profile)

# DynamoDB
dynamodb = session.resource('dynamodb', region_name='ap-northeast-1', endpoint_url="https://dynamodb.ap-northeast-1.amazonaws.com")

user = dynamodb.Table('dev-simple-blog')

hashIdArray = [
'aaabbbb1111'
]

# versionの更新
for hashId in hashIdArray:
    update_version(hashId)
    # 1秒スリープ
    time.sleep(1)

ほとんど公式ドキュメントにあったサンプルをそのまま引っ張ってきています。やっていることはとても単純で aaabbbbb11111 のパーティションキーに合致するデータの _vesrion を1インクリメントしています。

ちなみにDynamoDBのテーブルはこのような感じです。とても単純。

スクリーンショット 2016-11-09 18.03.17

出力

$ python update_increment.py
aaabbbb1111, update_version: 12

成功しました。ここにたどり着くまでに色々と試行錯誤してみたのですが、なかなかうまくいかず3時間くらい悩んでいました。大事だったのは ExpressionAttributeNames で変数の置き換えを利用することでした。

ExpressionAttributeNames

ExpressionAttributeNames を利用することで基本的にどのような項目名でも、なんらかの変数(プレースホルダー)に変換して名称を参照させることができます。特に今回の例のように記号を含む場合や、項目名がとても長い場合など、適切な長さの変数や読みやすいものに変換してやることで、ソースが非常に見通しの良くなります。 update_item_atomic_counter_version *1 なんて項目名がついていたとしたら、これらを何度も記述するのはとても面倒ですしね。

NGだった例

以下の例は、正常に実行できなかったスクリプトたちです。

ドキュメントの例をそのまま借用

# versionを強制的に+1する
def update_version(hashId):
    response = user.update_item(
        Key= {
            'sub': hashId
        },
        UpdateExpression="set _version = _version + :val",
        ExpressionAttributeValues={
            ':val':decimal.Decimal(1),
        },
        ReturnValues="UPDATED_NEW"
    )
    print('%s, update_version: %d' % (hashId, response.get('Attributes', {}).get('_version', -99)))

実行結果

botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the UpdateItem operation: Invalid UpdateExpression: Syntax error; token: "_", near: "set _version"

_ 始まりはSyntax Errorになりました。 UpdateExpression は先頭に記号がある項目名などは利用できません。 これがそのまま使えれば何の悩みもなかったのですが、当然ながらそうも行かず。

ExpressionAttributeValuesを使う

ExpressionAttributeValues こちらは値を置き換えることのできるパラメータです。こちらを利用してみます。少々無茶ですが、value の置き換えができるのだからオブジェクトも入れられるのでは?と考え、無理やり {'N' : '_vesrion'} を入力してみました。

def update_version(hashId):
    response = user.update_item(
        Key= {
            'sub': hashId
        },
        UpdateExpression="set #ver = #ver + :val",
        ExpressionAttributeValues={
            ':val':decimal.Decimal(1),
      '#ver':{'N':'_version'}
        },
        ReturnValues="UPDATED_NEW"
    )
    print('%s, update_version: %d' % (hashId, response.get('Attributes', {}).get('_version', -99)))

実行結果

botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the UpdateItem operation: ExpressionAttributeValues contains invalid key: Syntax error; key: "#ver"

名称として指定した #ver がNGのようです。Valueではないので当然といえば当然かもしれません。

まとめ

ExpressionAttributeNamesExpressionAttributeValues のパラメータをうまく使うことが鍵でした。通常の演算では入力できない項目名や、同じ値を何度も使う場合など、それぞれうまく変数を定義することができるようです。なかなか公式の簡単なサンプルなどにはないので正解を探すのに苦労しましたが、一度知ってしまえば色々と応用の利きそうです。

参照

脚注

  1. 名称の是非は置いておいて