キー設計パターン
このガイドでは、DynamoDBにおけるエンティティのパーティションキー(PK)とソートキー(SK)の設計方法を説明します。適切なキー設計は、パフォーマンス、スケーラビリティ、クエリ効率にとって重要です。
このガイドを使用するタイミング
以下が必要な場合にこのガイドを使用してください:
- 新しいエンティティタイプのキーを設計する
- 親子関係をモデル化する(Order → OrderItems)
- マルチテナントデータ分離をサポートする
- 効率的なクエリパターンを有効にする(テナント別リスト、日付フィルター)
- 楽観的ロック用のバージョニングを処理する
- エンティティ定義パターン - これらのキーパターンを使用するエンティティの定義方法
- マルチテナントパターン - テナント分離とクロステナント操作
- バックエンド開発ガイド - 完全なモジュール実装パターン
このパターンが解決する問題
| 問題 | 解決策 |
|---|---|
| テナントの全アイテムをクエリするのが遅い | パーティションレベルの分離のためにPKにテナントコードを含める |
| すべてのキーを知らないと子アイテムをリストできない | 異なるSKプレフィックスで共有PKを使用する |
| IDが作成時間でソートできない | 一意性と時間ソートの両方を持つULIDを使用する |
| 同時更新でバージョン競合が発生する | SKのバージョンサフィックスで楽観的ロックを有効にする |
パターン選択ガイド
このデシジョンツリーを使用して、ユースケースに適したキーパターンを選択してください:
デシジョンマトリックス
| 要件 | 推奨パターン | PK構造 | SK構造 |
|---|---|---|---|
| テナント分離を伴うシンプルなCRUD | シンプルエンティティ | ENTITY#tenantCode | ulid() |
| 複数の子を持つ親 | 階層構造 | PARENT#tenantCode | TYPE#parentId[#childId] |
| 複数のエンティティバリアント | 複合SK | ENTITY#tenantCode | variant#identifier |
| クロステナント共有データ | 共通テナント | ENTITY#common | tenantCode#identifier |
| カテゴリ別設定 | マスターデータ | MASTER#tenantCode | TYPE#category#code |
| 時間ベースのクエリ | 時系列 | LOG#tenantCode#YYYY-MM | timestamp#eventId |
デシジョンツリー
開始: どのような種類のデータを保存しますか?
│
├─ スタンドアロンエンティティ(商品、顧客)
│ └─ シンプルエンティティパターンを使用
│
├─ 親子関係(注文 → 明細)
│ └─ 子は独立したアクセスが必要ですか?
│ ├─ はい → 参照付きの別PKを使用
│ └─ いいえ → 階層パターン(共有PK)を使用
│
├─ 設定/マスターデータ
│ └─ マスターデータパターンを使用
│
├─ 時間ベースのイベント(ログ、監査)
│ └─ 時系列パターンを使用
│
└─ 複数のバリアントを持つユーザー/エンティティ
└─ 複合SKパターンを使用
キー構造の概要
フレームワークは一貫したキー構造を使用します:
PK = PREFIX#TENANT_CODE
SK = IDENTIFIER[@VERSION]
ID = PK#SK (without version)
KEY_SEPARATOR定数(#)はキーコンポーネントを区切るために使用されます。
フレームワーク定数
フレームワークは@mbc-cqrs-serverless/coreで以下の定数を提供します:
| 定数 | 値 | 説明 |
|---|---|---|
KEY_SEPARATOR | # | キーコンポーネントを区切る(PKセグメント、SKセグメント、ID) |
VER_SEPARATOR | @ | ソートキーとバージョン番号を区切る |
VERSION_FIRST | 0 | 新しいエンティティの初期バージョン |
VERSION_LATEST | -1 | 最新バージョンのクエリを示す |
TENANT_COMMON | common | 共有・テナント横断データ用のテナントコード(非推奨 — DEFAULT_COMMON_TENANT_CODES を使用) |
DEFAULT_COMMON_TENANT_CODES | ['common'] | 共通テナントコードのリスト(環境変数 COMMON_TENANT_CODES で設定可能) |
DEFAULT_TENANT_CODE | single | シングルテナントモードのデフォルトテナント |
@mbc-cqrs-serverless/masterと@mbc-cqrs-serverless/tenantパッケージはSettingTypeEnum.TENANT_COMMON = 'common'(小文字)を使用しており、getUserContext()のテナントコード正規化と一貫しています。これにより、createCommonTenantSetting()やcreateCommonTenant()メソッドで保存されたデータを正しくクエリできます。
組み込みキージェネレーター
フレームワークは以下の組み込みキージェネレーターを提供します:
import { masterPk, seqPk, ttlSk } from "@mbc-cqrs-serverless/core";
// Master data partition key (マスターデータパーティションキー)
masterPk("tenant001"); // "MASTER#tenant001"
masterPk(); // "MASTER#single" (default tenant)
// Sequence partition key (シーケンスパーティションキー)
seqPk("tenant001"); // "SEQ#tenant001"
// TTL sort key for table-level TTL settings (テーブルレベルTTL設定用のTTLソートキー)
ttlSk("product"); // "TTL#product"
基本的なキー生成
コアパッケージからユーティリティをインポートします:
import {
generateId,
getTenantCode,
KEY_SEPARATOR,
VER_SEPARATOR,
removeSortKeyVersion,
addSortKeyVersion,
getSortKeyVersion,
VERSION_FIRST,
VERSION_LATEST,
TENANT_COMMON,
} from "@mbc-cqrs-serverless/core";
import { ulid } from "ulid";
キーの生成
const PRODUCT_PK_PREFIX = "PRODUCT";
// Generate PK (PKを生成)
const pk = `${PRODUCT_PK_PREFIX}${KEY_SEPARATOR}${tenantCode}`;
// 結果: "PRODUCT#tenant001"
// Generate SK (using ULID for uniqueness and sortability) (SKを生成(一意性とソート可能性のためにULIDを使用))
const sk = ulid();
// 結果: "01HX7MBJK3V9WQBZ7XNDK5ZT2M"
// Generate ID (combination of PK and SK) (IDを生成(PKとSKの組み合わせ))
const id = generateId(pk, sk);
// 結果: "PRODUCT#tenant001#01HX7MBJK3V9WQBZ7XNDK5ZT2M"
バージョン管理
// Add version to SK (SKにバージョンを追加)
const skWithVersion = addSortKeyVersion(sk, 3);
// 結果: "01HX7MBJK3V9WQBZ7XNDK5ZT2M@3"
// Remove version from SK (SKからバージョンを削除)
const baseSk = removeSortKeyVersion(skWithVersion);
// 結果: "01HX7MBJK3V9WQBZ7XNDK5ZT2M"
// Get version number from SK (SKからバー ジョン番号を取得)
const version = getSortKeyVersion(skWithVersion);
// 結果: 3
// Get version from SK without version suffix (バージョンサフィックスなしのSKからバージョンを取得)
const latestVersion = getSortKeyVersion(sk);
// 結果: -1 (VERSION_LATEST)
テナントコード抽出
import { getTenantCode } from "@mbc-cqrs-serverless/core";
// Extract tenant code from PK (PKからテナントコードを抽出)
const tenantCode = getTenantCode("PRODUCT#tenant001");
// 結果: "tenant001"
// Returns undefined if no separator found (セパレーターが見つからない場合はundefinedを返す)
const noTenant = getTenantCode("PRODUCT");
// 結果: undefined
一般的なキーパ ターン
パターン1: シンプルなエンティティ
ユースケース: 商品カタログ
シナリオ: テナントに属する商品を一意のIDで保存します。
使用タイミング: 親子関係のないスタンドアロンエンティティ。
// Key Structure (キー構造)
PK: PRODUCT#<tenantCode>
SK: <ulid>
// 例
PK: PRODUCT#tenant001
SK: 01HX7MBJK3V9WQBZ7XNDK5ZT2M
ID: PRODUCT#tenant001#01HX7MBJK3V9WQBZ7XNDK5ZT2M
const pk = `PRODUCT${KEY_SEPARATOR}${tenantCode}`;
const sk = ulid();
const id = generateId(pk, sk);
パターン2: 階層エンティティ
ユースケース: 明細付き注文
シナリオ: 注文には複数のアイテムが含まれます。注文のすべてのアイテムを効率的にクエリする必要があります。
解決策: 親と子の間でPKを共有し、SKプレフィックスでアイテムタイプを区別します。
// Order Key Structure (注文キー構造)
PK: ORDER#<tenantCode>
SK: ORDER#<orderId>
// Order Item Key Structure (same PK, different SK prefix) (注文アイテムキー構造(同じPK、異なるSKプレフィックス))
PK: ORDER#<tenantCode>
SK: ORDER_ITEM#<orderId>#<itemId>
// 例
Order:
PK: ORDER#tenant001
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M
Order Items:
PK: ORDER#tenant001
SK: ORDER_ITEM#01HX7MBJK3V9WQBZ7XNDK5ZT2M#001
SK: ORDER_ITEM#01HX7MBJK3V9WQBZ7XNDK5ZT2M#002
const ORDER_SK_PREFIX = "ORDER";
const ORDER_ITEM_SK_PREFIX = "ORDER_ITEM";
// Create order (注文を作成)
const orderPk = `ORDER${KEY_SEPARATOR}${tenantCode}`;
const orderId = ulid();
const orderSk = `${ORDER_SK_PREFIX}${KEY_SEPARATOR}${orderId}`;
// Create order item (注文アイテムを作成)
const itemSk = `${ORDER_ITEM_SK_PREFIX}${KEY_SEPARATOR}${orderId}${KEY_SEPARATOR}${itemId}`;
パターン3: 複数認証プロバイダーを持つユーザー
ユースケース: 統合ユーザーアイデンティティ
シナリオ: ユーザーはローカルパスワード、SSO、またはOAuthでサインインできます。すべての認証方法を1人のユーザーにリンクする必要があります。
解決策: すべてのユーザーレコードに同じPKを使用し、SKプレフィックスで認証プロバイダーを示します。
// Key Structure (キー構造)
PK: USER#<tenantCode>
SK: <provider>#<userId>
// 例
PK: USER#common
SK: local#user123 // ローカル認証
SK: sso#abc123def456 // SSOプロバイダー
SK: oauth#google789 // OAuthプロバイダー
SK: temp#session456 // 一時セッション
SK: profile#user123 // ユーザープロファイルデータ
type AuthProvider = "local" | "sso" | "oauth" | "temp" | "profile";
function generateUserSk(provider: AuthProvider, userId: string): string {
return `${provider}${KEY_SEPARATOR}${userId}`;
}
const pk = `USER${KEY_SEPARATOR}common`;
const sk = generateUserSk("sso", cognitoSubId);
パターン4: マルチテナント関連付け
ユースケース: ユーザーが複数の組織に所属
シナリオ: SaaSアプリケーションでは、1人のユーザーが複数のテナント/組織に所属できます。
解決策: テナントコードとユーザーコードを組み合わせたSKを持つ共通テナントを使用します。
// Key Structure (キー構造)
PK: USER_TENANT#<commonTenant>
SK: <tenantCode>#<userCode>
// 例
PK: USER_TENANT#common
SK: tenant001#user123
SK: tenant002#user123 // 異なるテナントの同一ユーザー
const pk = `USER_TENANT${KEY_SEPARATOR}common`;
const sk = `${tenantCode}${KEY_SEPARATOR}${userCode}`;
パターン5: カテゴリ付きマスターデータ
ユースケース: アプリケーション設定と構成
シナリオ: メールテンプレート、商品カテゴリ、アプリケーション設定を保存します。
解決策: SKにタイププレフィックス(SETTING、DATA)を使用して、異なる設定タイプを整理します。
// Key Structure (キー構造)
PK: MASTER#common
SK: <type>#<category>#<code>
// Types: SETTING, DATA, COPY (タイプ: SETTING、DATA、COPY)
// Master data is shared across all tenants under the common partition (マスターデータはcommonパーティションの下で全テナント間で共有されます)
// 例
PK: MASTER#common
SK: SETTING#notification#email_template
SK: DATA#product_category#electronics
SK: DATA#product_category#clothing
SK: COPY#backup#2024-01-01
const SETTING_PREFIX = "SETTING";
const DATA_PREFIX = "DATA";
function generateMasterSk(type: string, category: string, code: string): string {
return `${type}${KEY_SEPARATOR}${category}${KEY_SEPARATOR}${code}`;
}
const pk = `MASTER${KEY_SEPARATOR}common`;
const sk = generateMasterSk(DATA_PREFIX, "product_category", "electronics");
パターン6: 時系列データ
ユースケース: アクティビティログと監査証跡
シナリオ: 日付範囲でクエリする必要があるタイムスタンプ付きイベントを保存します。
解決策: 時間ベースのパーティショニングのためにPKに日付を含め、ソートのためにSKにタイムスタンプを含めます。
// Key Structure (キー構造)
PK: LOG#<tenantCode>#<year-month>
SK: <timestamp>#<eventId>
// 例
PK: LOG#tenant001#2024-01
SK: 2024-01-15T10:30:00Z#evt001
SK: 2024-01-15T10:31:00Z#evt002
function generateLogKeys(tenantCode: string, timestamp: Date, eventId: string) {
const yearMonth = timestamp.toISOString().slice(0, 7); // "2024-01"
const pk = `LOG${KEY_SEPARATOR}${tenantCode}${KEY_SEPARATOR}${yearMonth}`;
const sk = `${timestamp.toISOString()}${KEY_SEPARATOR}${eventId}`;
return { pk, sk };
}
キーヘルパー関数
一貫したキー生成のためのヘルパーファイルを作成します:
// helpers/key.ts
import { KEY_SEPARATOR, generateId } from "@mbc-cqrs-serverless/core";
import { ulid } from "ulid";
// Entity prefixes (エンティティプレフィックス)
export const PRODUCT_PK_PREFIX = "PRODUCT";
export const ORDER_PK_PREFIX = "ORDER";
export const ORDER_SK_PREFIX = "ORDER";
export const ORDER_ITEM_SK_PREFIX = "ORDER_ITEM";
export const USER_PK_PREFIX = "USER";
export const MASTER_PK_PREFIX = "MASTER";
export const NOTIFICATION_PK_PREFIX = "NOTIFICATION";
// Key generators (キージェネレーター)
export function generateProductPk(tenantCode: string): string {
return `${PRODUCT_PK_PREFIX}${KEY_SEPARATOR}${tenantCode}`;
}
export function generateOrderPk(tenantCode: string): string {
return `${ORDER_PK_PREFIX}${KEY_SEPARATOR}${tenantCode}`;
}
export function generateOrderSk(orderId?: string): string {
const id = orderId ?? ulid();
return `${ORDER_SK_PREFIX}${KEY_SEPARATOR}${id}`;
}
export function generateOrderItemSk(orderId: string, itemId: string): string {
return `${ORDER_ITEM_SK_PREFIX}${KEY_SEPARATOR}${orderId}${KEY_SEPARATOR}${itemId}`;
}
// Key parsers (キーパーサー)
export function parseOrderSk(sk: string): { prefix: string; orderId: string } {
const parts = sk.split(KEY_SEPARATOR);
return {
prefix: parts[0],
orderId: parts[1],
};
}
export function parseOrderItemSk(sk: string): {
prefix: string;
orderId: string;
itemId: string;
} {
const parts = sk.split(KEY_SEPARATOR);
return {
prefix: parts[0],
orderId: parts[1],
itemId: parts[2],
};
}
// ID generator with entity type (エンティティタイプ付きIDジェネレーター)
export function generateEntityId(
prefix: string,
tenantCode: string,
sk?: string,
): { pk: string; sk: string; id: string } {
const pk = `${prefix}${KEY_SEPARATOR}${tenantCode}`;
const finalSk = sk ?? ulid();
const id = generateId(pk, finalSk);
return { pk, sk: finalSk, id };
}
バージョン管理
フレームワークは楽観的ロックのためにバージョニングを使用します:
// DynamoDB Tables (DynamoDBテーブル)
// Command table: Stores all versions with @version suffix (コマンドテーブル: @バージョンサフィックス付きで全バージョンを保存)
// Data table: Stores latest version only (no version suffix) (データテーブル: 最新バージョンのみ保存(バージョンサフィックスなし))
// Version in SK (Command table) (SKのバージョン(コマンドテーブル))
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M@1 // バージョン1
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M@2 // バージョン2
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M@3 // バージョン3
// Data table SK (no version suffix) (データテーブルSK(バージョンサフィックスなし))
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M
import {
VERSION_FIRST,
VERSION_LATEST,
addSortKeyVersion,
removeSortKeyVersion,
} from "@mbc-cqrs-serverless/core";
// Creating first version (最初のバージョンを作成)
const command = new OrderCommandDto({
pk,
sk,
version: VERSION_FIRST, // 0
// ...
});
// Reading specific version from history (履歴から特定のバージョンを読み取り)
const skWithVersion = addSortKeyVersion(baseSk, 2);
const historicalItem = await historyService.getItem({ pk, sk: skWithVersion });
// In Data Sync Handler - always remove version for RDS storage (データ同期ハンドラーで - RDS保存時は常にバージョンを削除)
async up(cmd: CommandModel): Promise<any> {
const sk = removeSortKeyVersion(cmd.sk);
// Store sk without version in RDS (バージョンなしのSKをRDSに保存)
}
クエリパターン
PKによるクエリ
// Get all products for a tenant (テナントの全商品を取得)
const items = await dataService.listItemsByPk(
`PRODUCT${KEY_SEPARATOR}${tenantCode}`,
);
SKプレフィックスによるクエリ
// Get all order items for a specific order (特定の注文の全注文アイテムを取得)
const items = await dataService.listItemsByPk(
`ORDER${KEY_SEPARATOR}${tenantCode}`,
{
sk: {
skExpression: 'begins_with(sk, :skPrefix)',
skAttributeValues: { ':skPrefix': `ORDER_ITEM${KEY_SEPARATOR}${orderId}` },
},
},
);
SK範囲によるクエリ
// Get orders within a date range (if using timestamp in SK) (SKにタイムスタンプを使用している場合、日付範囲内の注文を取得)
const items = await dataService.listItemsByPk(
`ORDER${KEY_SEPARATOR}${tenantCode}`,
{
sk: {
skExpression: 'sk BETWEEN :start AND :end',
skAttributeValues: { ':start': startDate, ':end': endDate },
},
},
);