DynamoDBテーブルのSet型でタグ的な項目を管理する

DynamoDBテーブルでタグ的な項目を管理したくなったので、DynamoDBのSet型を試してみました。
2021.07.27

IoT機器を管理する方法のひとつとして、タグを検討しています。たとえば下記です。

  • 機器1: 工場A、1階、担当X
  • 機器2: 工場A、1階、担当Z
  • 機器3: 工場B、2階、担当Z、机の上

ユーザが任意にタグを付けて、自由に管理できるイメージです。

これらの管理をDynamoDBテーブルで実現する方法として、DynamoDBのSet型を試してみました。

おすすめの方

  • DynamoDBでSet型を使いたい方

ToDoテーブルのサンプル

userId todoId title tags
u0001 t0001 電池を買う 買い物, Amazon, スーパー
u0001 t0002 映画を見る 映画, Amazon
u0001 t0003 ちくわを買う 買い物, スーパー
u0001 t0004 AWS認定試験を申し込む 勉強, AWS, 趣味
u1111 t0005 熊の置物を掘る 趣味, プレゼント
u1111 t0006 ラーメンを食べる 趣味, ご飯

DynamoDBテーブルをデプロイする

CloudFormationテンプレート

dynamodb.yaml

AWSTemplateFormatVersion: "2010-09-09"
Description: Todo DynamoDB Sample

Resources:
  TodoTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: todo-tag-sample-table
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: userId
          AttributeType: S
        - AttributeName: todoId
          AttributeType: S
      KeySchema:
        - AttributeName: userId
          KeyType: HASH
        - AttributeName: todoId
          KeyType: RANGE

デプロイ

aws cloudformation deploy \
	--template-file dynamodb.yaml \
	--stack-name Todo-Tag-DynamoDB-Sample-stack

DynamoDBテーブルを操作する

データ追加

下記でデータを追加します。tags部分はset型です。

insert.py

import boto3

INSERT_ITEMS = [
    {
        'userId': 'u0001',
        'todoId': 't0001',
        'title': '電池を買う',
        'tags': {'買い物', 'Amazon', 'スーパー'}
    },
    {
        'userId': 'u0001',
        'todoId': 't0002',
        'title': '映画を見る',
        'tags': {'映画', 'Amazon'}
    },
    {
        'userId': 'u0001',
        'todoId': 't0003',
        'title': 'ちくわを買う',
        'tags': {'買い物', 'スーパー'}
    },
    {
        'userId': 'u0001',
        'todoId': 't0004',
        'title': 'AWS認定試験を申し込む',
        'tags': {'勉強', 'AWS', '趣味'}
    },
    {
        'userId': 'u1111',
        'todoId': 't0005',
        'title': '熊の置物を掘る',
        'tags': {'趣味', 'プレゼント'}
    },
    {
        'userId': 'u1111',
        'todoId': 't0006',
        'title': 'ラーメンを食べる',
        'tags': {'趣味', 'ご飯'}
    },
]

def main():
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('todo-tag-sample-table')

    insert_items(table)


def insert_items(table):
    for item in INSERT_ITEMS:
        table.put_item(Item=item)


if __name__ == '__main__':
    main()

DynamoDBテーブルの様子です。tagsStringSetになっています。DynamoDBのドキュメントではSSと表記される場合もあります。

DynamoDBテーブルにデータが追加された

タグはSet型になっている

データ検索

特定ユーザで、全データを取得する

userIdを指定してquery()を実行します。

query_user_all.py

import boto3
from boto3.dynamodb.conditions import Key


def main():
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('todo-tag-sample-table')

    ret = get_all(table, 'u0001')

    dump(ret)


def get_all(table, user_id):
    options = {
        'KeyConditionExpression': Key('userId').eq(user_id),
    }
    data = []
    while True:
        res = table.query(**options)
        data += res.get('Items', [])
        if 'LastEvaluatedKey' not in res:
            break
        options['ExclusiveStartKey'] = res['LastEvaluatedKey']
    return data


def dump(items):
    for item in items:
        print(item)


if __name__ == '__main__':
    main()
$ python query_user_all.py
{'todoId': 't0001', 'userId': 'u0001', 'tags': {'Amazon', '買い物', 'スーパー'}, 'title': '電池を買う'}
{'todoId': 't0002', 'userId': 'u0001', 'tags': {'Amazon', '映画'}, 'title': '映画を見る'}
{'todoId': 't0003', 'userId': 'u0001', 'tags': {'買い物', 'スーパー'}, 'title': 'ちくわを買う'}
{'todoId': 't0004', 'userId': 'u0001', 'tags': {'趣味', '勉強', 'AWS'}, 'title': 'AWS認定試験を申し込む'}

特定ユーザで、特定タグを含むデータを取得する

userIdtagsを指定してquery()を実行します。

query_user_tags.py

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


def main():
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('todo-tag-sample-table')

    ret = get_include_tag(table, 'u0001', 'スーパー')

    dump(ret)


def get_include_tag(table, user_id, tag):
    options = {
        'KeyConditionExpression': Key('userId').eq(user_id),
        'FilterExpression': Attr('tags').contains(tag),
    }
    data = []
    while True:
        res = table.query(**options)
        data += res.get('Items', [])
        if 'LastEvaluatedKey' not in res:
            break
        options['ExclusiveStartKey'] = res['LastEvaluatedKey']
    return data


def dump(items):
    for item in items:
        print(item)


if __name__ == '__main__':
    main()
$ python query_user_tags.py
{'todoId': 't0001', 'userId': 'u0001', 'tags': {'買い物', 'スーパー', 'Amazon'}, 'title': '電池を買う'}
{'todoId': 't0003', 'userId': 'u0001', 'tags': {'スーパー', '買い物'}, 'title': 'ちくわを買う'}

また、下記のように複数条件を指定可能です。

options = {
    'KeyConditionExpression': Key('userId').eq(user_id),
    'FilterExpression': Attr('tags').contains('買い物') & Attr('tags').contains('Amazon'),
}
options = {
    'KeyConditionExpression': Key('userId').eq(user_id),
    'FilterExpression': Attr('tags').contains('買い物') | Attr('tags').contains('Amazon'),
}

データ更新

タグを追加する

addの更新式でタグを追加します。

update_add_tags.py

import boto3
from boto3.dynamodb.conditions import Attr


def main():
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('todo-tag-sample-table')

    update(table, 'u0001', 't0001', '単三')

def update(table, user_id, todo_id, tag):
    options = {
        'Key': {
            'userId': user_id,
            'todoId': todo_id,
        },
        'UpdateExpression': 'add #tags :tag',
        'ExpressionAttributeNames': {
            '#tags': 'tags',
        },
        'ExpressionAttributeValues': {
            ':tag': {tag}
        },
    }
    table.update_item(**options)


if __name__ == '__main__':
    main()

「単三」が追加されました。

タグが追加された

タグを削除する

deleteの更新式でタグを削除します。

update_remove_tags.py

import boto3
from boto3.dynamodb.conditions import Attr


def main():
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('todo-tag-sample-table')

    update(table, 'u0001', 't0001', 'スーパー')

def update(table, user_id, todo_id, tag):
    options = {
        'Key': {
            'userId': user_id,
            'todoId': todo_id,
        },
        'UpdateExpression': 'delete #tags :tag',
        'ExpressionAttributeNames': {
            '#tags': 'tags',
        },
        'ExpressionAttributeValues': {
            ':tag': {tag}
        },
    }
    table.update_item(**options)


if __name__ == '__main__':
    main()

「スーパー」が削除されました。

タグが削除された

さいごに

DynamoDBのSet型を試してみました。いい感じに使えそうです。

参考