なになれ

IT系のことを記録していきます。

DynamoDBのスキーマ設計について学んだこと

DynamoDBのスキーマ設計について公式ドキュメントのベストプラクティスなどを見ながら学んだ記録です。

スキーマ設計の考え方

DynamoDBのスキーマ設計はRDBとは異なり、以下の考え方を採用します。

  • テーブルを分けずに1つのテーブルを使う
  • PartitionKeyとSortKeyで1対多の関係を表現する
  • インデックスやSortKeyを用いて、検索条件に対応する

スキーマ設計

テーブル設計

partitionKey sortKey attribute
posts post-id title,content,author
post-id comment-id content,author
  • 記事とコメントの1:多の関係はPartitionKeyとSortKeyで定義できる
  • 全記事を取得するにはPartitionKeyにpostsを指定する
  • 個別の記事を取得するにはPartitionKeyにpostsとSortKeyにpost-idを指定する
  • コメントを取得するにはPartitionKeyにpost-idを指定する

インデックス設計

indexKey partitionKey sortKey Attribute
user-id posts post-id title,content,author
  • 特定のユーザが書いた記事を取得するにはindexKeyにuser-idを指定する

インデックスを使わないパターン

インデックスを使わずにSortKeyだけでも同様のことが実現可能です。
SortKeyはbegins_with関数で特定の文字列から始まるクエリを記述することが可能です。
これを利用して、以下のように複合キーにします。

partitionKey sortKey attribute
posts user-id#post-id title,content,author

user-idをクエリの条件にすることで特定のユーザが書いた記事を取得可能です。

ただこのテーブル設計の場合、個別の記事を取得するのに事前にユーザが分かっていなければならないという問題点があります。
一方で、インデックスの作成数には制限があります。インデックスを使うのか、テーブル設計で対応するのか、使い分けが必要になります。
個人的には特定の値に依存しない汎用的なインデックスを作ってしまうのが良いと思います。

実践

今回はブログを想定した記事とコメントのデータ構造を題材とします。
以下のような構造です。
f:id:hi1280:20190617212423p:plain

ローカルのDynamoDBをインストールする

以下を参考にして、ローカルにDynamoDBをインストールします。

コンピュータ上の DynamoDB (ダウンロード可能バージョン) - Amazon DynamoDB

テーブルを作成する

今回はNode.js上でawssdkを使い、DynamoDBを操作します。

create-table.js

const AWS = require("aws-sdk");
const util = require("util");

AWS.config.update({
  region: "us-west-2",
  endpoint: "http://localhost:8000"
});

const dynamodb = new AWS.DynamoDB();
const params = {
  TableName: "Sample",
  KeySchema: [
    { AttributeName: "partitionKey", KeyType: "HASH" },
    { AttributeName: "sortKey", KeyType: "RANGE" }
  ],
  AttributeDefinitions: [
    { AttributeName: "partitionKey", AttributeType: "S" },
    { AttributeName: "sortKey", AttributeType: "S" }
  ],
  BillingMode: "PAY_PER_REQUEST"
};

async function createTable() {
  const createTable = util.promisify(dynamodb.createTable).bind(dynamodb);
  const data = await createTable(params);
  console.log(
    "Created table. Table description JSON:",
    JSON.stringify(data, null, 2)
  );
}

createTable();

partitionKeyとsortKeyを文字列型の属性で作成します。

インデックスを作成する

create-index.js

// 省略

const dynamodb = new AWS.DynamoDB();
const params = {
  TableName: "Sample",
  AttributeDefinitions: [{ AttributeName: "indexKey", AttributeType: "S" }],
  GlobalSecondaryIndexUpdates: [
    {
      Create: {
        IndexName: "index1",
        KeySchema: [
          {
            AttributeName: "indexKey",
            KeyType: "HASH"
          }
        ],
        Projection: {
          ProjectionType: "ALL"
        },
        ProvisionedThroughput: {
          ReadCapacityUnits: "0",
          WriteCapacityUnits: "0"
        }
      }
    }
  ]
};

async function createIndex() {
  const updateTable = util.promisify(dynamodb.updateTable).bind(dynamodb);
  const data = await updateTable(params);
  console.log(
    "update table. Table description JSON:",
    JSON.stringify(data, null, 2)
  );
}

createIndex();

グローバルセカンダリインデックスをindex1という名前で作成します。
このインデックスではindexKeyというpartitionKeyを持つことが定義されています。
これにより、indexKeyの値と等価の条件で項目を検索することが可能になります。

データをロードする

load-data.js

// 省略

const fs = require("fs");
const docClient = new AWS.DynamoDB.DocumentClient();

const allData = JSON.parse(fs.readFileSync(__dirname + "/data.json", "utf8"));
function loadData() {
  allData.forEach(async data => {
    const params = {
      TableName: "Sample",
      Item: {
        partitionKey: data.partitionKey,
        sortKey: data.sortKey,
        attribute: data.attribute,
        indexKey: data.indexKey
      }
    };
    const put = util.promisify(docClient.put).bind(docClient);
    await put(params);
    console.log("PutItem succeeded:", data.partitionKey);
  });
}

loadData();

data.json

[
  {
    "partitionKey": "posts",
    "sortKey": "post-1",
    "attribute": {
      "title": "記事のタイトル",
      "content": "内容",
      "author": "書いた人"
    },
    "indexKey": "user-1"
  },
  {
    "partitionKey": "posts",
    "sortKey": "post-2",
    "attribute": {
      "title": "記事のタイトル2",
      "content": "内容2",
      "author": "書いた人2"
    },
    "indexKey": "user-2"
  },
  {
    "partitionKey": "posts",
    "sortKey": "post-3",
    "attribute": {
      "title": "記事のタイトル3",
      "content": "内容3",
      "author": "書いた人"
    },
    "indexKey": "user-1"
  },
  {
    "partitionKey": "post-1",
    "sortKey": "comment-1",
    "attribute": {
      "content": "コメント内容",
      "author": "書いた人"
    }
  },
  {
    "partitionKey": "post-1",
    "sortKey": "comment-2",
    "attribute": {
      "content": "コメント内容2",
      "author": "書いた人2"
    }
  },
  {
    "partitionKey": "post-2",
    "sortKey": "comment-1",
    "attribute": {
      "content": "コメント内容3",
      "author": "書いた人3"
    }
  }
]

data.jsonの値を読み込んでDynamoDBに登録します。

クエリを実行する

query.js

// 省略

const docClient = new AWS.DynamoDB.DocumentClient();

async function query(params) {
  const query = util.promisify(docClient.query).bind(docClient);
  const data = await query(params);
  data.Items.forEach(function(item) {
    console.log(JSON.stringify(item));
  });
}

async function exec() {
  console.log("全記事を取得");
  await query({
    TableName: "Sample",
    KeyConditionExpression: "partitionKey = :value",
    ExpressionAttributeValues: {
      ":value": "posts"
    }
  });
  console.log("特定の記事を取得");
  await query({
    TableName: "Sample",
    KeyConditionExpression:
      "partitionKey = :partitionValue and sortKey = :sortValue",
    ExpressionAttributeValues: {
      ":partitionValue": "posts",
      ":sortValue": "post-1"
    }
  });
  console.log("特定のユーザの全記事を取得");
  await query({
    TableName: "Sample",
    IndexName: "index1",
    KeyConditionExpression: "indexKey = :value",
    ExpressionAttributeValues: {
      ":value": "user-1"
    }
  });
  console.log("特定の記事の全コメントを取得");
  await query({
    TableName: "Sample",
    KeyConditionExpression: "partitionKey = :partitionValue",
    ExpressionAttributeValues: {
      ":partitionValue": "post-1"
    }
  });
}

exec();

スキーマ設計時に想定していた検索ができることを確認します。

まとめ

DynamoDBを利用する場合には事前にスキーマ設計を考えなければいけないです。
どのような検索条件が必要かといったことを踏まえて設計を考える必要があり、RDBのようにリレーショナルな形で設計するよりも難しいように思います。

参考情報