【AWS DynamoDB】サーバレスアプリケーションを開発するためにDynamoDBのテーブル設計した

# DynamoDBって何？

DynamoDBは、AWSが提供するキーバリュー・ドキュメント指向のNoSQL DBサービスです。従来のRDB（リレーショナルデータベース）とは異なり、スキーマレスな設計や高速なパフォーマンス、自動的なスケーリングが特徴です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711491/7e812492-37b7-42a9-a064-1883263b2df2.png)

## DynamoDBの主な特徴

* フルマネージド: サーバーの管理やメンテナンスが不要
* 高速なパフォーマンス: SSDストレージによる高速な読み書き
* 高いスケーラビリティ: 自動的なスケールイン/スケールアウト
* 柔軟なデータモデル: スキーマレスな設計で、様々なデータ構造に対応

:::note
個人的にはAPIでデータの登録・更新・削除ができるのはとても良い点だなと思っています。
:::

## DynamoDBのユースケース

* 高トラフィックなWebアプリケーション
* モバイルアプリケーションのバックエンド
* IoTアプリケーション
* ゲームアプリケーション

# サーバーレスアプリケーションって何？

サーバーレスアプリケーションとは、サーバーの管理を必要としないアプリケーションのことです。AWS Lambdaなどのサーバーレスサービスを利用することで、インフラの管理から解放され、開発者はアプリケーションのロジックに集中できます。

## サーバーレスアプリケーションのメリット

* 高いスケーラビリティ: 需要に応じて自動的にスケール
* コスト効率: 従量課金制で、使用した分だけ支払い
* 開発効率: インフラの管理が不要で、開発に集中できる

:::note
比較的少ない人数での稼働が想定される時などは、サーバーやRDBを使用するよりも圧倒的に安いです。
:::

## サーバーレスアプリケーションのデメリット

* コールドスタート: 初回アクセス時にレイテンシが発生
* デバッグの複雑さ: 分散アーキテクチャのため、デバッグが難しい場合がある

:::note
AWS SAMを使用して、ローカル環境でテストコードを実行しておくことで、保守性は高められます。
ここでのテストコードは、PythonだったらPytestなどです。
:::

# NoSQLとリレーショナルDBの違い

NoSQLとリレーショナルデータベース（RDB）は、それぞれ異なる特性を持っています。NoSQLはスキーマレスな設計や高いスケーラビリティが特徴であり、RDBは厳格なスキーマやACID特性による高いデータ整合性が特徴です。

## データモデルの違い

* NoSQL: スキーマレスで、柔軟なデータモデル
* RDB: 厳格なスキーマで、テーブル間の関係を定義

:::note
RDBの場合、カラムを追加するときにマイグレーションなどの操作が必要になりますが、NoSQLの場合はプライマリーキーを指定しておけばOKです。
:::

## スケーラビリティとパフォーマンス

* NoSQL: 水平スケーリングが得意で、高いパフォーマンス
* RDB: 垂直スケーリングが主で、複雑なクエリが得意

:::note
RDBはインデックスの設定などである程度のパフォーマンス改善ができますが、NoSQLの場合は難しい場合があります。
:::

## ユースケースに基づく選択

* NoSQL: 高トラフィックなWebアプリケーションやIoTなど、大量のデータを高速に処理する場合
* RDB: 会計システムや顧客管理システムなど、データの整合性が重要な場合

:::note
RDBはJOINなどをうまく使うことでクエリが大量に実行されてしまう「N+1問題」が発生しないようにすることができますが、NoSQLはJOINができないので、テーブル数ができるだけ少なくなるように設計を行う必要があります。
:::

# DynamoDBテーブル設計

今回のアプリケーションでは、ユーザーグループ、ユーザー、タスクを管理するために、以下のテーブルを設計しました。

## DynamoDBを使用してやりたいこと

やりたいこととしては以下です。

* 全てのレコードの取得
* ある条件に合致する全てのレコードの取得
* 特定の単一レコードの取得

この3パターンのデータの取得が可能となるように設計を行う必要があります。

### 全てのレコードを取得する方法

DynamoDBには`scan`というAPIがあり、これを叩くことで、テーブルの全てのデータが取得できます。

ドキュメントには、以下のように記載されています。

> 指定されたテーブルまたはインデックスのすべての項目を取り出します。項目全体またはその属性のサブセットのみを取り出すことができます。オプションでフィルタリング条件を適用すると、関心のある値のみを返し、残りは破棄できます。

ランタイムがPythonのLambda関数を使用している場合、以下のように記載することができます。

```py
import boto3
from boto3.dynamodb.conditions import Attr

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

    options = {
        'FilterExpression': Attr('カラム名').contains('検索する値'),
    }
    res = table.scan(**options)   # scan() と指定した場合、テーブルのすべてのデータを取得します。

~以下省略~
```

### ある条件に合致する全てのレコードの取得

DynamoDBには`query`というAPIがあり、これを叩くことで、テーブルの全てのデータが取得できます。

ドキュメントには、以下のように記載されています。

> 特定のパーティションキーがあるすべての項目を取り出します。パーティションキーの値を指定する必要があります。項目全体またはその属性のサブセットのみを取り出すことができます。オプションで、ソートキーの値に条件を適用し、同じパーティションキーがあるデータのサブセットだけを取り出すことができます。テーブルにパーティションキーとソートキーの両方を持つテーブルがある場合、テーブルでこのオペレーションを使用できます。また、インデックスにパーティションキーとソートキーの両方がある場合、インデックスでこのオペレーションを使用できます。

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

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

    options = {
        'KeyConditionExpression': Key('パーティションキーのカラム名').eq('検索する値'),
        # 必要ならFilterExpressionも追加できます（キー以外での追加絞り込み）
        # 'FilterExpression': Attr('他のカラム名').contains('データ'),
    }

    res = table.query(**options)  # query() はパーティションキーが必須です
```

### 特定の単一レコードの取得

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

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

    options = {
        'KeyConditionExpression': Key('パーティションキーのカラム名').eq('検索する値') & Key('ソートキーのカラム名').begins_with('検索する値'),
        # 必要に応じて追加のフィルタも可能
        # 'FilterExpression': Attr('他のカラム名').contains('データ'),
    }

    res = table.query(**options)
```

## DynamoDBからレコードを取得する上での注意点

これは個人的にですが、以下については注意すべきかなと思っています。

* 原則として、「パーティションキーのみ」「パーティションキー＋ソートキー」の検索のみの処理に限定する

その理由として、「**キーに指定していないカラムでの絞り込みは動作が遅くなる可能性がある**からです。
キーに指定しているカラムでの絞り込みは、**インデックスを使って絞り込む**ため、検索処理が高速です。

:::note warn
決して、キーに指定していないカラムでの絞り込みをしてはいけないということではないです。
:::

検索の順序として、「パーティションキーのみ」「パーティションキー＋ソートキー」の絞り込みの後に、キーに指定していないカラムでの絞り込みを行うべきです。これであれば、動作が遅くなってしまうことを防げます。

| 条件タイプ | 使うメソッド | 処理速度 | 読み込みコスト | 備考 |
| --- | --- | --- | --- | --- |
| パーティションキー | KeyConditionExpression | ◎ | 超高速 | 最小限 | 必須条件 |
| パーティション + ソートキー | KeyConditionExpression | ◎ | 高速 | 最小限 | 条件付きクエリ |
| 属性によるフィルタ | FilterExpression | △（後処理） | 減らない | レスポンス件数は減る |

## 設計したテーブル一覧

今回は、以下のテーブルについて設計しました。

* UserGroupsTable: ユーザーグループ情報を管理
* UsersTable: ユーザー情報を管理
* TasksTable: タスク情報を管理

## 各テーブルの設計詳細

### UserGroupsTable

| 項目 | 内容 |
|---|---|
| パーティションキー | `group_id` |
| 属性 | `group_id` (String), `group_name` (String) |
| 説明 | ユーザーグループ情報を保持するテーブル   |

### UsersTable

| 項目 | 内容 |
|---|---|
| パーティションキー | `group_id` |
| ソートキー | `user_id` |
| 属性 | `group_id` (String), `user_id` (String), `email` (String), `name` (String), `password` (String) |
| 説明 | ユーザー情報を保持するテーブル。1つのグループに複数のユーザーが所属 |

### TasksTable

| 項目 | 内容 |
|---|---|
| パーティションキー | `user_id` |
| ソートキー | `task_id` |
| 属性 | `user_id` (String), `task_id` (String), `task_name` (String), `task_description` (String), `due_date` (String), `status` (Integer), `priority` (Integer) |
| 説明 | タスク情報を保持するテーブル。ユーザーごとにタスクを管理 |

## DynamoDBテーブルのスキーマファイル

以下は、各テーブルのスキーマを定義したYAMLファイルです。各テーブルの主キー（パーティションキーとソートキー）と属性、説明を定義しています。（このファイルは開発をスムーズに進めるために作成しただけのものです。）

```YAML
tables:
  - name: user-groups
    primaryKey:
      partitionKey: group_id
    attributes:
      - name: group_id
        type: String
    description: ユーザグループ情報を保持するテーブル

  - name: users
    primaryKey:
      partitionKey: group_id
      sortKey: user_id
    attributes:
      - name: group_id
        type: String
      - name: user_id
        type: String
      - name: email
        type: String
      - name: name
        type: String
      - name: password
        type: String
    description: ユーザ情報を保持するテーブル。ユーザは1つのグループに所属する。

  - name: tasks
    primaryKey:
      partitionKey: user_id
      sortKey: task_id
    attributes:
      - name: user_id
        type: String
      - name: task_id
        type: String
      - name: task_name
        type: String
      - name: task_description
        type: String
      - name: due_date
        type: String
      - name: status
        type: Integer
      - name: priority
        type: Integer
    description: タスク情報を保持するテーブル。ユーザ単位でタスクを管理する。
```

# まとめ

RDBに比べて、設計が難しい...！