DynamoDBの高レベルクライアント「PynamoDB」が良かった

Pythonを使ったDynamoDBの操作には、公式SDKのBoto3を使うことが多いかと思います。Boto3で提供されているのは低レベルなクライアントなため、柔軟な操作ができるという利点はありますが、単純な操作でも記述が複雑になりがちで独自ラッパークラスを作っちゃう問題がありました。

最近PynamoDBという高レベルなクライアントライブラリを見つけて、使ってみたら最高にいい感じだったので基本的な使い方をまとめます。

- テーブル操作
    - [テーブルモデル定義](#テーブルモデル定義)
    - [テーブル作成](#テーブル作成)
    - [テーブル削除](#テーブル削除)
- アイテム操作
    - [アイテム作成](#アイテム作成)
    - [アイテムのクエリ検索](#アイテムのクエリ検索)
    - [キーを指定したアイテムの取得](#キーを指定したアイテムの取得)
    - [キーあたりのアイテム数取得](#キーあたりのアイテム数取得)
    - [アイテムの更新](#アイテムの更新)
    - [アイテムの削除](#アイテムの削除)
    - [条件付き操作](#条件付き操作)

## 環境
- macOS 10.12.6
- python 3.6.1
- pynamodb 3.2.1

## 参考
[PynamoDBのドキュメント](https://pynamodb.readthedocs.io/en/latest/index.html)とServerless FrameworkのExampleに「[aws-python-rest-api-with-pynamodb](https://github.com/serverless/examples/tree/master/aws-python-rest-api-with-pynamodb)」というのがあったのでそれらを参考にしています。

## インストール

```bash
$ pip install pynamodb
```


## テーブルモデル定義
まずテーブル定義をします。こんな感じでDjangoのDBモデルっぽく書けます。

```todo_model.py
from datetime import datetime

from pynamodb.attributes import UnicodeAttribute, BooleanAttribute, UTCDateTimeAttribute
from pynamodb.models import Model


class ToDoModel(Model):
    class Meta:
        table_name = 'todo_table'
        region = 'ap-northeast-1'
        write_capacity_units = 1
        read_capacity_units = 1

    # テーブル定義
    createdBy = UnicodeAttribute(hash_key=True, null=False)
    createdAt = UTCDateTimeAttribute(range_key=True, null=False, default=datetime.now())
    text = UnicodeAttribute(null=False)
    checked = BooleanAttribute(null=False, default=False)
    updatedAt = UTCDateTimeAttribute(null=False, default=datetime.now())
```

**Meta**クラスにテーブルのリソース定義をします。
localstackなどでローカルに起動したDynamoDBに接続する場合は`host = 'http://localhost:8000'`と書いておくと指定したホスト・ポートで起動しているDynamoDBが使えるようになります。
テーブル定義の部分では型の定義の他に、ハッシュキーやレンジキーを指定したり、デフォルト値をセットしたり、nullを許すかどうかということが定義できます。


## テーブル作成
`create_table()`メソッドでモデルをもとにテーブルの作成ができます。
構成の設定情報がコードに入ってくるとリソース管理しづらくてイケてないので、これは動作確認などお試しで使うテーブルを作成する時ですね。

```todo_app.py
from todo_model import ToDoModel

if not ToDoModel.exists():
    ToDoModel.create_table(wait=True)
```
`exists()`でテーブルの有無が取得できます。
`wait=True`を指定するとはテーブル作成が完了するのを待ちます。
テーブルのキャパシティを引数に指定して`create_table()`するとモデルで定義したキャパシティ設定を上書きできます。

```todo_app.py
if not ToDoModel.exists():
    ToDoModel.create_table(read_capacity_units=2, write_capacity_units=2, wait=True)
```

## テーブル削除
`delete_table()`でテーブル削除できます。

```todo_app.py
ToDoModel.delete_table()
```

## アイテム作成
まずアイテムオブジェクトを作成し、オブジェクトの`save()`メソッドでテーブルに書き込みます。

```todo_app.py
a_todo = ToDoModel('Taro', text='Buy books.')

a_todo.save()
```

最初の2つの引数は自動でハッシュキー、レンジキーの順番で認識されます。レンジキーがないテーブルの場合または今回の例のようにレンジキーにデフォルト値に設定している場合は、いれなくてOKです。

実行するとアイテムが作成されています。
<img width="823" alt="スクリーンショット 2018-03-18 19.35.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/99248/0078131a-7284-ec90-3c7c-66251a80dcb4.png">


## アイテムのクエリ検索
ハッシュキーでクエリ検索し、for文でアイテムごとに処理を行っています。


```todo_app.py
for item in ToDoModel.query('Taro'):
    print("createdBy:{0}, createdAt:{1}".format(item.createdBy, item.createdAt))
# createdBy:Taro, createdAt:2018-03-18 19:35:43.531462+00:00
# createdBy:Taro, createdAt:2018-03-18 19:37:00.568644+00:00
# createdBy:Taro, createdAt:2018-03-18 19:37:50.249173+00:00
```

レンジキーを合わせたクエリ

```todo_app.py
for item in ToDoModel.query('Taro', ToDoModel.createdAt > datetime(2018, 3, 18, 19, 37)):
    print("createdBy:{0}, createdAt:{1}".format(item.createdBy, item.createdAt))
# createdBy:Taro, createdAt:2018-03-18 19:37:00.568644+00:00
# createdBy:Taro, createdAt:2018-03-18 19:37:50.249173+00:00
```

アンド条件を使ったクエリ

```todo_app.py
for item in ToDoModel.query('Taro', (ToDoModel.checked == False) & (ToDoModel.text.contains('book'))):
    ...
```

## キーを指定したアイテムの取得
`ToDoModel.get()`の引数にハッシュキー（とレンジキー）を指定することでアイテムオブジェクトとして取得できます。

```todo_app.py
a_todo = ToDoModel.get('Taro', datetime(2018, 3, 18, 19, 35, 43, 531462))
print(a_todo.text)
# Buy books.
```

## キーあたりのアイテム数取得
```todo_app.py
print(ToDoModel.count('Taro'))
# 12
```


## アイテムの更新
`update()`の引数にactionリストで複数項目の更新ができます。

```todo_app.py
a_todo = ToDoModel.get('Taro', datetime(2018, 3, 18, 19, 35, 43, 531462))

a_todo.update(actions=[
    ToDoModel.text.set('Go to the library.')
])

print(a_todo.text)
# Go to the library.
```

<img width="794" alt="スクリーンショット 2018-03-18 19.46.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/99248/a4fe9aa9-ff59-f3e7-9485-9fc778d1ee73.png">
更新されています。

## アイテムの削除
```todo_app.py
a_todo.delete()
```

## 条件付き操作
```todo_app.py
a_todo.delete(ToDoModel.checked == True) # a_todo の checked が Trueだった場合アイテムを削除
```

条件の記述方法はドキュメントの[Condition Expressions](https://pynamodb.readthedocs.io/en/latest/conditional.html#conditions)に記載されています。

## まとめ
裏でどんなクエリを送信しているか見えづらいので、実際にプロダクション等で使うときはそこまで気にして使う必要があると思います。
アップデートも活発なようで今後も使っていきたい所存。