python boto3でDynamoDBを操作する場合の自分向けチートシート

# 概要
毎回boto3でDynamoDBを操作する場合の書き方を忘れるので、自分用のチートシートを作成します。


# 想定するテーブル
本ページで扱うテーブル情報は以下とします。
学校の科目ごとの成績を管理するテーブルです。

|項目|内容|備考|
|--|--|--|
|テーブル名|school_score_table||
|パーティションキー(PK)|student|生徒ごとにユニークな名称|
|ソートキー(SK)|subject|科目はMath, English, Scienceなど|
|グローバル セカンダリ インデックス(GSI)|subject-Index|科目だけでクエリするためのインデックス|

入れておくレコードはこんな想定です。
```python
[
    {
        'student': 'Alice',
        'subject': 'Math',
        'grade': 'A'
    },
    {
        'student': 'Alice',
        'subject': 'English',
        'grade': 'B'
    },
    {
        'student': 'Bob',
        'subject': 'Math',
        'grade': 'C'
    },
    {
        'student': 'Bob',
        'subject': 'English',
        'grade': 'A'
    }
]
```

# DynamoDBのテーブル設定
最初のテーブル設定部分です。
```URL```はパラメータストアやLambda環境変数から取ってくるのが良いです。
また、DynamoDBのテーブル操作が頻繁に行われるのであれば、テーブル名を変数や引数としたクラス化や関数化してあげるとスマートな気がします。

```python
import boto3
from boto3.dynamodb.conditions import Key

URL = 'http://dynamodb.ap-northeast-1.amazonaws.com/xxxxxxxxxxxx/'
TABLE_NAME = 'school_score_table'

dynamodb = boto3.resource('dynamodb', endpoint_url=URL)
table = dynamodb.Table(TABLE_NAME)
```


# テーブル操作
## get_item
```get_item()```は、テーブルから１つのレコードを取得します。
１つのレコードのため、応答は辞書型となります。

```get_item()```はレコードが一意に決まる```Key```を設定します。
今回はPKとSKでレコードが一意に決まるため、PKとSKを指定しています。
SKがない場合（PKだけで一意）の場合には、SKを指定する必要はありません。

```python
response = table.get_item(
    Key={
        'student': 'Alice',
        'subject': 'English'
    }
)
print(response['Item'])
# {'student': 'Alice', 'subject': 'English', 'grade': 'B'}
```


## query_item
```query()```は、テーブルから条件に一致するレコードを```KeyConditionExpression```で指定してリストで取得します。
クエリの仕方はいくつかバリエーションがあります。

### PKだけでクエリする
```python
response = table.query(
    KeyConditionExpression=Key('student').eq('Alice')
)

print(response['Items'])
# [{'student': 'Alice', 'subject': 'Math', 'grade': 'A'}, {'student': 'Alice', 'subject': 'English', 'grade': 'B'}]
```

### GSIでクエリする
```python
response = table.query(
    IndexName='subject-Index',
    KeyConditionExpression=Key('subject').eq('Math')
)

print(response['Items'])
# [{'student': 'Alice', 'subject': 'Math', 'grade': 'A'}, {'student': 'Bob', 'subject': 'Math', 'grade': 'C'}]
```

:::note warn
```query()```では、```FilterExpression```を使ってクエリ結果の絞り込みができます。ただし、```FilterExpression```の有無にかかわらず、使用されるRead Capacity Unit(読取容量)は変化しません。
:::


## put_item
```put_item()```は、テーブルに１つのレコードを追加します。
既に同じ主キーを持つレコードがある場合は、古いアイテムを書き換えます。

```python
table.put_item(
    Item={
        'student': 'Charly',
        'subject': 'Math',
        'grade': 'B'
    }
)
```

:::note warn
新しい項目が既存の項目を置き換えるのを防ぐには、```attribute_not_exists```関数と、PKの属性名を含む条件式を使用します。

以下は```student```がない場合のみ、レコードを追加する処理の例です。
```python
table.put_item(
    Item=item,
    ConditionExpression='attribute_not_exists(student)'
)
```
:::


## update_item
```update_item()```は、指定したレコードを更新します。
指定したキーがない場合は、レコードを新たに追加します。

```python
table.update_item(
    Key={
        'student': 'Alice',
        'subject': 'Math'
    },
    UpdateExpression='SET grade = A+', # string型のプロパティはUpdateExpressionのみでOK
)
```

### String型以外のプロパティの更新方法
```UpdateExpression```は、string型のみ使用できます。
number型のプロパティを更新したい場合は、```ExpressionAttributeValues```を使います。

```python
table.update_item(
    Key={
        'student': 'Alice',
        'subject': 'Math'
    },
    UpdateExpression='SET score = :val', # :valという変数を定義
    ExpressionAttributeValues={
        ':val': 90 # 変数に入れる値を設定する
    }
)
```

また、```ExpressionAttributeNames```を指定することで、```UpdateExpression```のプロパティ名を変数として定義できます。

```python
table.update_item(
    Key={
            'student': 'Alice',
            'subject': 'Math'
    },
    UpdateExpression='SET #key_name1 = :val1, #key_name2 = :val2',
    ExpressionAttributeNames={
        '#key_name1': 'score',
        '#key_name2': 'grade',
    },
    ExpressionAttributeValues={
        ':val1': '90',
        ':val2': 'A+'
    }
)
```

### 更新する条件を指定する
```ConditionExpression```を使うと、更新する条件を指定できます。
以下は```score```の現在値が更新値よりも大きい場合のみ更新します。

```python
table.update_item(
    Key={
            'student': 'Alice',
            'subject': 'Math'
        },
    ConditionExpression='#key_name1 > :val1',
    UpdateExpression='SET #key_name1 = :val1, #key_name2 = :val2',
    ExpressionAttributeNames={
        '#key_name1': 'score',
        '#key_name2': 'grade',
    },
    ExpressionAttributeValues={
        ':val1': '90',
        ':val2': 'A+'
    }
)
```

また、```ConditionExpression```を満たさない場合、```ConditionalCheckFailedException```により書込みが失敗します。


## delete_item
```delete_item()```は、テーブルのレコードを１つ削除します。
指定の仕方は```get_item()```と同じように```Key```を使います。

```python
table.delete_table(
    Key={
        'student': 'Alice',
        'subject': 'English'
    }
)
```

# まとめ
自分の習熟度に応じて、随時更新していきたいと思います。