ちょっと話題の記事

DynamoDBのテーブル設計に最適!NoSQL WorkbenchのData modelerで今度こそDynamoDBを使いこなす!

最近使ってNoSQL Workbenchは最高のツールだと分かったので紹介します!
2020.11.06

はじめに

CX事業本部の佐藤智樹です。

今回はAWSが提供しているDynamoDB用のアプリ「NoSQL Workbench」の機能を使ってデータモデリングする流れを解説します。

最近案件でテーブル設計を再検討する必要がありNoSQL Workbenchを使ったところ、サンプルデータを入れながら設計が正しいか検証でき非常に便利だったので紹介します。

他の記事でもアプリの紹介はありますが本記事ではデータモデリングに絞って解説を行います。題材として多対多のデータをモデリングしながら設計する方法を紹介します。

本記事を読めば今までDynamoDBのデータ設計に悩んでいた方の検討時間をかなり減らすことができます。自分ももっと早く「NoSQL WorkbenchのData modeler はこう使って欲しい!」という記事があれば良かったなあと思ったので記事にしました。

NoSQL Workbenchとは

全体的な概要は以下の記事で確認することができます。今回は以下の記事にある「Data modeler」と「Visualizer」を使っていきます。

データモデリング

本章からデータモデリングを解説していきます。本来設計する場合は必要な工程(ER図に落とし込む部分やユースケース/アクセスパターンの洗い出しなど)もあるのですが、NoSQL Workbenchの紹介に主眼を置くため簡単なサンプルに留めています。

実際にモデリングする場合は最初に以下の「サーバーレスアプリケーション向きの DB 設計ベストプラクティス」という資料を読むことを強くおすすめします。

ER図の作成

いきなり設計を始める前に、まずER図を書いてどんな対象がシステムに関わるのか整理するところから始める必要があります。 今回はデバイスエンティティとユーザエンティティが存在し、デバイスは複数のユーザと紐付き、人も複数のデバイスと紐付くシステムとして検討します。

ER図のデータが多対多の関係になっているので「隣接関係のリスト設計パターン」を参考に1テーブルで管理します。詳細は以下のドキュメントや記事が参考になるかと思います。

※ 今回はサンプルなので設計ありきで話を進めていますが設計を先に行うとユースケースを完全に網羅できるか分かりません。なので、「ユースケース/アクセスパターン列挙->NoSQL Workbenchで設計->データ投入->検証->設計修正」のサイクルを何度か実施するのが効率良さそうです。

ユースケースの洗い出し

NoSQLの設計原則として、RDBMSと違って「ビジネス上の問題とアプリケーションのユースケースを理解することが重要」とAWSのドキュメントにも書かれています。(NoSQL 設計の2つの重要な概念の部分)

DynamoDBへのアクセスパターンが増えることによってLSIやGSIが必要になるので重要な項目になります。設計する対象によって内容が異なるため今回は深掘りしませんが、リレーションのあるエンティティを扱う際は以下のドキュメントが参考になると思います。

ここでは単純に以下のパターンを想定して、最後にアクセスパターンを満たすか確認します。

  • ユーザの情報が取得できる
  • デバイスの情報が取得できる
  • ユーザに紐付く複数のデバイスを取得できる
  • デバイスに紐付く複数のユーザを取得できる

NoSQL Workbenchで設計

本章から実際にNoSQL Workbenchの機能を使用していきます。 とりあえず試したい場合はおまけの章にデータモデルをエクスポートしたjsonを記載したのでインポートして使ってください。

テーブル設計の作成

まずNoSQL Workbenchを起動して「Data modeler」を選択後、「+ Create Data Model」で新しいデータモデルを作成します。

モデル名などの入力欄が出るので、覚えやすい名前などを設定します。今回はモデル名を「UserDeviceModel」として進めます。

すると「Data model」の欄に作成したモデルが追加されます。

作成したデータモデルの配下にテーブルや項目を作っていきます。画面の赤枠部分の「Add table」からテーブル定義を作成します。

Partition KeyやSort Key、Attributeを設定する画面が出るので入力します。テーブル名は「DEVICE」、Partition Keyは「firstId」、Sort Keyは「secondId」で設定します。

※ PK(firstId)とSK(secondId)にはER図のuserIdとdeviceIdの両方が入るので抽象的な名前にしています。

同ページの下の「+ add global secondary index」からGSIの設定が追加できます。N:1、1:Nのデータ取得を行うためにPK(secondId)、SK(firstId)のGSIを作成します。

上記のGSIはデバイスに紐付く複数のユーザを取得するために作成しています。気になる方は以下のスライドのP39をご確認ください。

最後に右下の「Add table definition」から設計を追加してください。

すると以下のようにテーブル定義が確認できます。

データの投入

次にテーブルにデータを投入します。左の「Visualizer」から先ほど作成したテーブルを選択し「Update」から行えます。

右上の「Add data」からレコードを追加してカラムに項目を入れて、右上の「save」で保存します。 このテーブルには三種類のレコードが含まれるようになります。赤枠がユーザとデバイスに紐づく情報を持つレコード、青枠がユーザ単体の情報を持つレコード、黄色枠がデバイス単体の情報を持つレコードです。

アクセスパターンを満たすか確認

上記のデータ設計が上手くいっているか確認します。テーブル名をクリックすると、テーブルのPK、SKやGSIでどんなデータが取れるのか確認できます。

ユーザの情報が取得できる/デバイスの情報が取得できる

firstIdとsecondIdの両方にuserIdを入れればユーザ情報が取得でき、deviceIdを入れればデバイス情報が取得できます。

ユーザに紐付く複数のデバイスを取得できる

Query APIでfirstIdにdeviceId(deviceIdAなど)を指定して検索すれば取得できます。取得したユーザのIDを元に上記のユーザ情報取得を行えば複数ユーザの情報取得もできます。ただN+1問題が起きるのでユースケースによって(ex.1人に数100のデバイスが紐付くなど)は負荷試験が必要という検討もできます。

後、secondIdがuserIdのデータも抽出されるので、userIdの値の先頭に固有の文字列を付けて「begins_with」で検索し除外するという検討もできます。

デバイスに紐付く複数のユーザを取得できる

こちらはGSIを使用して、先ほどと同様にQuery APIでsecondId(PK)にdeviceIdを使用することで抽出できます。

結論

設計はアクセスパターンを満たしており、実現可能であることが確認できました。

しかし、カラム名に抽象的な名前(firstId,secondId)が付いてしまうことが分かりました。 テーブルの可読性を上げるために、無理にシングルテーブルにせずデバイス単体の情報やユーザ単体の情報は別テーブルに分けてしまうのもひとつの手かと思います。

おまけ:データモデルの共有

少し複雑な設計行うとサンプルデータが手元で見れない場合はGSIを追加する際に苦労するかもしれません。NoSQL WorkbenchにはデータモデルのExport機能があります。右上の「Export data model」でjson形式になった情報が抽出でき、他の方でも「Import data model」を使って手元で確認することができます。

以下に今回作成したデータモデルのjsonを記載します。以下のようなファイルをGitでドキュメントとして継続的に管理することもできます。

UserDeviceModel.json

{
  "ModelName": "UserDeviceModel",
  "ModelMetadata": {
    "Author": "",
    "DateCreated": "Nov 05, 2020, 04:20 PM",
    "DateLastModified": "Nov 05, 2020, 05:49 PM",
    "Description": "",
    "Version": "1.0"
  },
  "DataModel": [
    {
      "TableName": "DEVICE",
      "KeyAttributes": {
        "PartitionKey": {
          "AttributeName": "firstId",
          "AttributeType": "S"
        },
        "SortKey": {
          "AttributeName": "secondId",
          "AttributeType": "S"
        }
      },
      "NonKeyAttributes": [
        {
          "AttributeName": "deviceType",
          "AttributeType": "S"
        },
        {
          "AttributeName": "label",
          "AttributeType": "S"
        },
        {
          "AttributeName": "firstName",
          "AttributeType": "S"
        },
        {
          "AttributeName": "lastName",
          "AttributeType": "S"
        }
      ],
      "GlobalSecondaryIndexes": [
        {
          "IndexName": "secondIdFirstIdIndex",
          "KeyAttributes": {
            "PartitionKey": {
              "AttributeName": "secondId",
              "AttributeType": "S"
            },
            "SortKey": {
              "AttributeName": "firstId",
              "AttributeType": "S"
            }
          },
          "Projection": {
            "ProjectionType": "KEYS_ONLY"
          }
        }
      ],
      "TableData": [
        {
          "firstId": {
            "S": "userIdA"
          },
          "secondId": {
            "S": "deviceIdA"
          }
        },
        {
          "firstId": {
            "S": "userIdA"
          },
          "secondId": {
            "S": "userIdA"
          },
          "firstName": {
            "S": "太郎"
          },
          "lastName": {
            "S": "鈴木"
          }
        },
        {
          "firstId": {
            "S": "deviceIdA"
          },
          "secondId": {
            "S": "deviceIdA"
          },
          "deviceType": {
            "S": "raspi"
          },
          "label": {
            "S": "Myラズパイ"
          }
        },
        {
          "firstId": {
            "S": "userIdA"
          },
          "secondId": {
            "S": "deviceIdB"
          }
        },
        {
          "firstId": {
            "S": "deviceIdB"
          },
          "secondId": {
            "S": "deviceIdB"
          },
          "deviceType": {
            "S": "awsIot1Click"
          },
          "label": {
            "S": "Myボタン"
          }
        },
        {
          "firstId": {
            "S": "userIdB"
          },
          "secondId": {
            "S": "deviceIdB"
          }
        },
        {
          "firstId": {
            "S": "userIdB"
          },
          "secondId": {
            "S": "userIdB"
          },
          "firstName": {
            "S": "樹"
          },
          "lastName": {
            "S": "田中"
          }
        }
      ],
      "DataAccess": {
        "MySql": {}
      }
    }
  ]
}

感想

ユースケース/アクセスパターンの洗い出しも大変ですが、終わった後にハマる設計を構築するのにどうしても時間がかかります。 NoSQL Workbenchは「テーブル設計->アクセスパターン確認->修正」のサイクルを何度も高速に回せるかなり有用なツールだと感じています。

少し複雑な設計パターンの話と混ぜて書いてしまったので難しそうであれば、別の簡単な内容でNoSQL Workbench試してみてください。

自分自身もまだ使いこなせていない機能が沢山あるので今後紹介していきたいと思います。