DynamoDB GSIに対してLambdaからクエリを実行し特定の属性値を取得する

# はじめに
本記事ではDynamoDBのGSI（Global Secondary Index）を作成し、Lambdaからクエリを実行する処理を実装していきます。

# ゴール
DynamoDBのGSIに対してLambdaからクエリを実行し、パーティションキーとソートキーの条件に一致する項目から、特定の属性値のみを取得します。
下記のようなGSIを作成し、`user1` の `user_status:active`を取得する処理を実装します。

▼GSI例
| userId（パーティションキー） | timestamp（ソートキー）   | user_status |
|--------|-------------|-------------|
| user1  | 2024-11-07  | active      |
| user2  | 2024-11-08  | active    |


# 作るもの
- DynamoDBのテーブル：1つ
    - GSI：1つ
- Lambda：1つ

# 書かない事
IAMの権限については本記事では記載しておりません。

# 1. DynamoDBのテーブルを作成
次の5つの属性を持つテーブルを作成します。
- id (パーティションキー)
- userId
- timestamp
- user_status
- category

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/44f9854a-9a07-72fd-b5f7-fd87561c139a.png)



# 2. GSIを作成

次の3つの属性を持つインデックスを作成します。

- userId （GSIのパーティションキー）
- timestamp （GSIのソートキー）
- user_status （取得したい属性、Include > 属性名で選択）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/93f21fab-620c-e4de-85a9-33d98313b4d8.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/a206b71b-96ef-16cd-81a6-265ce2629390.png)

次の図のようにインデックスタブから想定通りGSIを作成できている事を確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/d64bf397-6493-dc3b-8d81-57f09ca47016.png)



# 3. Lambdaを作成

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

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('test-1107-db')

def lambda_handler(event, context):
    body = json.loads(event['body'])
    user_id = body['userId']
    specific_date = body['timestamp']
    
    # GSIを使用したクエリ
    response = table.query(
        # インデックス名
        IndexName='userId-timestamp-index',
        # クエリ条件：userIdとtimestampが一致する項目を検索
        KeyConditionExpression=Key('userId').eq(user_id) & Key('timestamp').eq(specific_date),
        # user_statusのみを取得
        ProjectionExpression='user_status'
    )
    
    return {
        'statusCode': 200,
        'body': json.dumps(response['Items'])
    }
```

クエリ部分についてはAmazon DynamoDB > デベロッパーガイド記載の下記の部分を参考にしています。
> リソースインターフェイスを使った同じクエリ操作を短くして簡略化できます。
> import boto3
> from boto3.dynamodb.conditions import Key, Attr
> dynamodb = boto3.resource('dynamodb')
> table = dynamodb.Table('YourTableName')
> response = table.query(
>   KeyConditionExpression=Key('pk').eq('id#1') & Key('sk').begins_with('cart#'),
>   FilterExpression=Attr('name').eq('SomeName')
> )
> 引用：[デベロッパーガイド](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/programming-with-python.html)

:::note info
属性が値と等しいことを確認するために `boto3.dynamodb.conditions.Key` を使用しています。今回使用したのは等価比較を行うための `eq` です。

- 参考：[DynamoDB conditions](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/customizations/dynamodb.html#dynamodb-conditions)

:::

# 4. LambdaからGSIへのクエリを実行

test eventを作成してLabmdaを実行してみます。実行するイベントの内容は次の通りです。

```json:test event
{
  "body": "{\"userId\": \"user1\", \"timestamp\": \"2024-11-07\"}"
}
```

GSIに保存している次の項目の `user_status` を取得する想定です。
```:項目
id (文字列)：1
timestamp：2024-11-07
user_status：active
userId：user1
```

Lambdaを実行すると200で正常に下記のレスポンスを取得できました。
想定通り `userId:user1` の `user_status：active` のデータを取得できていることが確認できます。

```json:response
{
  "statusCode": 200,
  "body": "[{\"user_status\": \"active\"}]"
}
```


# 参考

https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.KeyConditionExpressions

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/customizations/dynamodb.html#dynamodb-customization-reference

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/LegacyConditionalParameters.KeyConditions.html#KeyConditionExpression.instead


https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/programming-with-python.html