DynamoDBはどんなCRUD操作が可能か確認する

# はじめに
[以前の記事](https://qiita.com/neruneruo/items/71ad99629e9b11648222)で書いたように、DynamoDBはちゃんとキャパシティ管理さえしてあげれば、サーバレスで高性能でかなり良い感じなデータベースだ。

ただし、データベースと言ってもRDBとは違うので、そちらに慣れている人にはハマりどころが多々ある。

DynamoDBではどんなCRUD操作が可能かを確認しておこう。

# 前提条件
前提条件はあまりないが、以下の記事くらいを読んでおくと入りが良いとおもう。

- 【Qiita】[AWS DynamoDBと仲良くなれるかもしれないまとめ](https://qiita.com/yu-croco/items/66fd1a662dd64b8eb348)

特に、LSI、GSIはちゃんと理解しておかないと混乱することになるのでしっかり覚えておこう。

# 事前準備
今回は、以下のような仕様のデータベースを管理する前提とする。
- 社員ID(項目名:```id```)をキーとする社員管理テーブルを想定
- 社員は複数の部署(項目名:```department```)に所属可能である
- 社員の年齢をデータベースに持っておく

上記から、ハッシュキーは```id```とする。
と同時に、2つ目の要件を満たすために、```department```をレンジキーに指定する。
さらに、どんな検索要件に対応できるか確認するために、GSIとLSIにdepartmentを設定したテーブルをそれぞれ用意する。
※これ、書いてから気付いたが、別にテーブル分けなくてもそれぞれのインデックスを設定すれば1つのテーブルでも実験できたな……。

上記のテーブルを用意するTerraformは以下のような感じで準備。

```HCL
################################################################################
# DynamoDB Table                                                               #
################################################################################
resource "aws_dynamodb_table" "test" {
  name           = "test-table"
  billing_mode   = "PROVISIONED"
  read_capacity  = 1
  write_capacity = 1
  hash_key       = "id"
  range_key      = "department"

  attribute {
    name = "id"
    type = "S"
  }

  attribute {
    name = "department"
    type = "S"
  }

  local_secondary_index {
    name               = "department_lsi"
    range_key          = "department"
    projection_type    = "ALL"
  }

  global_secondary_index {
    name               = "department_gsi"
    hash_key           = "department"
    write_capacity     = 1
    read_capacity      = 1
    projection_type    = "ALL"
  }
}
```

さらに、スクリプトで以下のような感じでベースとなるアイテムをロードしておく。

```Python
def prepare_001():
    try:
        with table.batch_writer() as batch:
            batch.put_item(Item={ 'id': '00001', 'department': '11111', 'age': 35 })
            batch.put_item(Item={ 'id': '00001', 'department': '22222', 'age': 35 })
            batch.put_item(Item={ 'id': '00002', 'department': '11111', 'age': 28 })
            batch.put_item(Item={ 'id': '00003', 'department': '11111', 'age': 34 })
            batch.put_item(Item={ 'id': '00004', 'department': '22222', 'age': 39 })
        return "OK"
    except Exception as error:
        return error
```

# 実験開始
## 重複したキーの項目をPUTする

```PYTHON
def test_001_01():
    try:
        table.put_item(Item={ 'id': '00001', 'department': '11111', 'age': 36 })
        return "OK"
    except Exception as error:
        return error
```

結果：重複したキーの項目が更新された。

## 単一項目の非キー項目のアップデート

```PYTHON
def test_001_02():
    try:
        option = {
            'Key': {'id': '00001', 'department': '11111'},
            'UpdateExpression': 'set #age = :age',
            'ExpressionAttributeNames': {
                '#age': 'age'
            },
            'ExpressionAttributeValues': {
                ':age': 37
            }
        }
        table.update_item(**option)
        return "OK"
    except Exception as error:
        return error
```

結果：問題なく実行可能。

## 単一項目のキー項目のアップデート

```PYTHON
def test_001_03():
    try:
        option = {
            'Key': {'id': '00001', 'department': '11111'},
            'UpdateExpression': 'set #department = :department, #age = :age',
            'ExpressionAttributeNames': {
                '#department': 'department',
                '#age': 'age'
            },
            'ExpressionAttributeValues': {
                ':department': "33333",
                ':age': 37
            }
        }
        table.update_item(**option)
        return "OK"
    except Exception as error:
        return error
```

結果：エラーになる。キー項目の更新は ```delete``` → ```put``` するしかないようだ(詳細は後述)。

```
'An error occurred (ValidationException) when calling the UpdateItem operation: One or more parameter values were invalid: Cannot update attribute department. This attribute is part of the key'
```

## ハッシュキーのみでqueryする

```PYTHON
def test_001_04():
    try:
        response = table.query(
            KeyConditionExpression=Key('id').eq('00001')
        )
        return response['Items']
    except Exception as error:
        return error

結果：同一ハッシュキーのアイテムが複数返却される。

## ハッシュキー＋レンジキーでqueryする

def test_001_05():
    try:
        response = table.query(
            KeyConditionExpression=Key('id').eq('00001') & Key('department').eq('22222')
        )
        return response['Items']
    except Exception as error:
        return error
```

結果：単一アイテムに絞り込まれて返却される

## ハッシュキー＋レンジキー以外の項目でqueryする

```PYTHON
def test_001_06():
    try:
        response = table.query(
            KeyConditionExpression=Key('id').eq('00001'),
            FilterExpression=Attr('age').eq(35)
        )
        return response['Items']
    except Exception as error:
        return error
```

結果: queryで指定した条件で絞ることができる

## LSIでレンジキーのみ指定してqueryする

```PYTHON
def test_001_07():
    try:
        response = table.query(
            IndexName="department_lsi",
            KeyConditionExpression=Key('department').eq('11111')
        )
        return response['Items']
    except Exception as error:
        return error
```

結果: 不可能。LSIの場合、ハッシュキーが必要になるためエラーになる。

```
'An error occurred (ValidationException) when calling the Query operation: Query condition missed key schema element: id'
```

## GSIでレンジキーのみ指定してqueryする

```PYTHON
def test_001_08():
    try:
        response = table.query(
            IndexName="department_gsi",
            KeyConditionExpression=Key('department').eq('11111')
        )
        return response['Items']
    except Exception as error:
        return error
```

結果: GSIの場合はハッシュキー不要なので、セカンダリインデックスで指定したアイテムが複数返却される

## GSIでレンジキーのみ指定してqueryしてさらに件数を2件までに絞る

```PYTHON
def test_001_09():
    try:
        response = table.query(
            IndexName="department_gsi",
            KeyConditionExpression=Key('department').eq('11111'),
            Limit=2
        )
        return response['Items']
    except Exception as error:
        return error
```

結果: 想定通りの件数で絞られる

## キー項目を更新できない代わりに、delete → put する

```PYTHON
def test_001_10():
    try:
        table.delete_item(Key={ 'id': '00001', 'department': '22222' })
        table.put_item(Item={ 'id': '00001', 'department': '33333', 'age': 36 })
        return "OK"
    except Exception as error:
        return error
```

結果: 問題なく動作。ただし、DynamoDBは結果整合でしかないため、delete前にput結果が取得されることを考慮しておく。