メインコンテンツまでスキップ

キー設計パターン

このガイドでは、DynamoDBにおけるエンティティのパーティションキー(PK)とソートキー(SK)の設計方法を説明します。適切なキー設計は、パフォーマンス、スケーラビリティ、クエリ効率にとって重要です。

このガイドを使用するタイミング

以下が必要な場合にこのガイドを使用してください:

  • 新しいエンティティタイプのキーを設計する
  • 親子関係をモデル化する(Order → OrderItems)
  • マルチテナントデータ分離をサポートする
  • 効率的なクエリパターンを有効にする(テナント別リスト、日付フィルター)
  • 楽観的ロック用のバージョニングを処理する
関連ドキュメント

このパターンが解決する問題

問題解決策
テナントの全アイテムをクエリするのが遅いパーティションレベルの分離のためにPKにテナントコードを含める
すべてのキーを知らないと子アイテムをリストできない異なるSKプレフィックスで共有PKを使用する
IDが作成時間でソートできない一意性と時間ソートの両方を持つULIDを使用する
同時更新でバージョン競合が発生するSKのバージョンサフィックスで楽観的ロックを有効にする

パターン選択ガイド

このデシジョンツリーを使用して、ユースケースに適したキーパターンを選択してください:

デシジョンマトリックス

要件推奨パターンPK構造SK構造
テナント分離を伴うシンプルなCRUDシンプルエンティティtenantCode#ENTITYulid()
複数の子を持つ親階層構造tenantCode#PARENTTYPE#parentId[#childId]
複数のエンティティバリアント複合SKtenantCode#ENTITYvariant#identifier
クロステナント共有データ共通テナントcommon#ENTITYtenantCode#identifier
カテゴリ別設定マスターデータMASTER#tenantCodeTYPE#category#code
時間ベースのクエリ時系列tenantCode#LOG#YYYY-MMtimestamp#eventId

デシジョンツリー

開始: どのような種類のデータを保存しますか?

├─ スタンドアロンエンティティ(商品、顧客)
│ └─ シンプルエンティティパターンを使用

├─ 親子関係(注文 → 明細)
│ └─ 子は独立したアクセスが必要ですか?
│ ├─ はい → 参照付きの別PKを使用
│ └─ いいえ → 階層パターン(共有PK)を使用

├─ 設定/マスターデータ
│ └─ マスターデータパターンを使用

├─ 時間ベースのイベント(ログ、監査)
│ └─ 時系列パターンを使用

└─ 複数のバリアントを持つユーザー/エンティティ
└─ 複合SKパターンを使用

キー構造の概要

フレームワークは一貫したキー構造を使用します:

PK = TENANT_CODE#PREFIX
SK = IDENTIFIER[@VERSION]
ID = PK#SK (without version)

KEY_SEPARATOR定数(#)はキーコンポーネントを区切るために使用されます。

フレームワーク定数

フレームワークは@mbc-cqrs-serverless/coreで以下の定数を提供します:

定数説明
KEY_SEPARATOR#キーコンポーネントを区切る(PKセグメント、SKセグメント、ID)
VER_SEPARATOR@ソートキーとバージョン番号を区切る
VERSION_FIRST0新しいエンティティの初期バージョン
VERSION_LATEST-1最新バージョンのクエリを示す
TENANT_COMMONcommon共有・テナント横断データ用のテナントコード(非推奨 — DEFAULT_COMMON_TENANT_CODES を使用)
DEFAULT_COMMON_TENANT_CODES['common']共通テナントコードのリスト(環境変数 COMMON_TENANT_CODES で設定可能)
DEFAULT_TENANT_CODEsingleシングルテナントモードのデフォルトテナント
一貫したテナントコード形式

@mbc-cqrs-serverless/master@mbc-cqrs-serverless/tenantパッケージはSettingTypeEnum.TENANT_COMMON = 'common'(小文字)を使用しており、getUserContext()のテナントコード正規化と一貫しています。これにより、createCommonTenantSetting()createCommonTenant()メソッドで保存されたデータを正しくクエリできます。

組み込みキージェネレーター

フレームワークは以下の組み込みキージェネレーターを提供します:

import { masterPk, seqPk, ttlSk } from "@mbc-cqrs-serverless/core";

// マスターデータパーティションキー
masterPk("tenant001"); // "MASTER#tenant001"
masterPk(); // "MASTER#single" (default tenant)

// シーケンスパーティションキー
seqPk("tenant001"); // "SEQ#tenant001"

// テーブルレベル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";

// PKを生成
const pk = `${tenantCode}${KEY_SEPARATOR}${PRODUCT_PK_PREFIX}`;
// Result: "tenant001#PRODUCT"

// SKを生成(一意性とソート可能性のためにULIDを使用)
const sk = ulid();
// Result: "01HX7MBJK3V9WQBZ7XNDK5ZT2M"

// IDを生成(PKとSKの組み合わせ)
const id = generateId(pk, sk);
// Result: "tenant001#PRODUCT#01HX7MBJK3V9WQBZ7XNDK5ZT2M"

バージョン管理

// SKにバージョンを追加
const skWithVersion = addSortKeyVersion(sk, 3);
// Result: "01HX7MBJK3V9WQBZ7XNDK5ZT2M@3"

// SKからバージョンを削除
const baseSk = removeSortKeyVersion(skWithVersion);
// Result: "01HX7MBJK3V9WQBZ7XNDK5ZT2M"

// SKからバージョン番号を取得
const version = getSortKeyVersion(skWithVersion);
// Result: 3

// バージョンサフィックスなしのSKからバージョンを取得
const latestVersion = getSortKeyVersion(sk);
// Result: -1 (VERSION_LATEST)

テナントコード抽出

import { getTenantCode } from "@mbc-cqrs-serverless/core";

// PKからテナントコードを抽出
const tenantCode = getTenantCode("tenant001#PRODUCT");
// Result: "tenant001"

// セパレーターが見つからない場合はundefinedを返す
const noTenant = getTenantCode("PRODUCT");
// Result: undefined

一般的なキーパターン

パターン1: シンプルなエンティティ

ユースケース: 商品カタログ

シナリオ: テナントに属する商品を一意のIDで保存します。

使用タイミング: 親子関係のないスタンドアロンエンティティ。

// キー構造
PK: <tenantCode>#PRODUCT
SK: <ulid>

// Example
PK: tenant001#PRODUCT
SK: 01HX7MBJK3V9WQBZ7XNDK5ZT2M
ID: tenant001#PRODUCT#01HX7MBJK3V9WQBZ7XNDK5ZT2M
const pk = `${tenantCode}${KEY_SEPARATOR}PRODUCT`;
const sk = ulid();
const id = generateId(pk, sk);

パターン2: 階層エンティティ

ユースケース: 明細付き注文

シナリオ: 注文には複数のアイテムが含まれます。注文のすべてのアイテムを効率的にクエリする必要があります。

解決策: 親と子の間でPKを共有し、SKプレフィックスでアイテムタイプを区別します。

// 注文キー構造
PK: <tenantCode>#ORDER
SK: ORDER#<orderId>

// 注文アイテムキー構造(同じPK、異なるSKプレフィックス)
PK: <tenantCode>#ORDER
SK: ORDER_ITEM#<orderId>#<itemId>

// Example
Order:
PK: tenant001#ORDER
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M

Order Items:
PK: tenant001#ORDER
SK: ORDER_ITEM#01HX7MBJK3V9WQBZ7XNDK5ZT2M#001
SK: ORDER_ITEM#01HX7MBJK3V9WQBZ7XNDK5ZT2M#002
const ORDER_SK_PREFIX = "ORDER";
const ORDER_ITEM_SK_PREFIX = "ORDER_ITEM";

// 注文を作成
const orderPk = `${tenantCode}${KEY_SEPARATOR}ORDER`;
const orderId = ulid();
const orderSk = `${ORDER_SK_PREFIX}${KEY_SEPARATOR}${orderId}`;

// 注文アイテムを作成
const itemSk = `${ORDER_ITEM_SK_PREFIX}${KEY_SEPARATOR}${orderId}${KEY_SEPARATOR}${itemId}`;

パターン3: 複数認証プロバイダーを持つユーザー

ユースケース: 統合ユーザーアイデンティティ

シナリオ: ユーザーはローカルパスワード、SSO、またはOAuthでサインインできます。すべての認証方法を1人のユーザーにリンクする必要があります。

解決策: すべてのユーザーレコードに同じPKを使用し、SKプレフィックスで認証プロバイダーを示します。

// キー構造
PK: <tenantCode>#USER
SK: <provider>#<userId>

// Examples
PK: common#USER
SK: local#user123 // Local authentication
SK: sso#abc123def456 // SSO provider
SK: oauth#google789 // OAuth provider
SK: temp#session456 // Temporary session
SK: profile#user123 // User profile data
type AuthProvider = "local" | "sso" | "oauth" | "temp" | "profile";

function generateUserSk(provider: AuthProvider, userId: string): string {
return `${provider}${KEY_SEPARATOR}${userId}`;
}

const pk = `common${KEY_SEPARATOR}USER`;
const sk = generateUserSk("sso", cognitoSubId);

パターン4: マルチテナント関連付け

ユースケース: ユーザーが複数の組織に所属

シナリオ: SaaSアプリケーションでは、1人のユーザーが複数のテナント/組織に所属できます。

解決策: テナントコードとユーザーコードを組み合わせたSKを持つ共通テナントを使用します。

// キー構造
PK: <commonTenant>#USER_TENANT
SK: <tenantCode>#<userCode>

// Example
PK: common#USER_TENANT
SK: tenant001#user123
SK: tenant002#user123 // Same user in different tenant
const pk = `common${KEY_SEPARATOR}USER_TENANT`;
const sk = `${tenantCode}${KEY_SEPARATOR}${userCode}`;

パターン5: カテゴリ付きマスターデータ

ユースケース: アプリケーション設定と構成

シナリオ: メールテンプレート、商品カテゴリ、アプリケーション設定を保存します。

解決策: SKにタイププレフィックス(SETTING、DATA)を使用して、異なる設定タイプを整理します。

// キー構造
PK: MASTER#COMMON
SK: <type>#<category>#<code>

// タイプ: SETTING、DATA、COPY
// マスターデータはCOMMONパーティションの下で全テナント間で共有されます
// Examples
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にタイムスタンプを含めます。

// キー構造
PK: <tenantCode>#LOG#<year-month>
SK: <timestamp>#<eventId>

// Example
PK: tenant001#LOG#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 = `${tenantCode}${KEY_SEPARATOR}LOG${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";

// エンティティプレフィックス
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";

// キージェネレーター
export function generateProductPk(tenantCode: string): string {
return `${tenantCode}${KEY_SEPARATOR}${PRODUCT_PK_PREFIX}`;
}

export function generateOrderPk(tenantCode: string): string {
return `${tenantCode}${KEY_SEPARATOR}${ORDER_PK_PREFIX}`;
}

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}`;
}

// キーパーサー
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ジェネレーター
export function generateEntityId(
tenantCode: string,
prefix: string,
sk?: string,
): { pk: string; sk: string; id: string } {
const pk = `${tenantCode}${KEY_SEPARATOR}${prefix}`;
const finalSk = sk ?? ulid();
const id = generateId(pk, finalSk);
return { pk, sk: finalSk, id };
}

バージョン管理

フレームワークは楽観的ロックのためにバージョニングを使用します:

// DynamoDBテーブル
// コマンドテーブル: @バージョンサフィックス付きで全バージョンを保存
// データテーブル: 最新バージョンのみ保存(バージョンサフィックスなし)

// SKのバージョン(コマンドテーブル)
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M@1 // Version 1
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M@2 // Version 2
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M@3 // Version 3

// データテーブルSK(バージョンサフィックスなし)
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M
import {
VERSION_FIRST,
VERSION_LATEST,
addSortKeyVersion,
removeSortKeyVersion,
} from "@mbc-cqrs-serverless/core";

// 最初のバージョンを作成
const command = new OrderCommandDto({
pk,
sk,
version: VERSION_FIRST, // 0
// ...
});

// 履歴から特定のバージョンを読み取り
const skWithVersion = addSortKeyVersion(baseSk, 2);
const historicalItem = await historyService.getItem({ pk, sk: skWithVersion });

// データ同期ハンドラーで - RDS保存時は常にバージョンを削除
async up(cmd: CommandModel): Promise<any> {
const sk = removeSortKeyVersion(cmd.sk);
// バージョンなしのSKをRDSに保存
}

クエリパターン

PKによるクエリ

// テナントの全商品を取得
const items = await dataService.listItemsByPk(
`${tenantCode}${KEY_SEPARATOR}PRODUCT`,
);

SKプレフィックスによるクエリ

// 特定の注文の全注文アイテムを取得
const items = await dataService.listItemsByPk(
`${tenantCode}${KEY_SEPARATOR}ORDER`,
{
sk: {
skExpression: 'begins_with(sk, :skPrefix)',
skAttributeValues: { ':skPrefix': `ORDER_ITEM${KEY_SEPARATOR}${orderId}` },
},
},
);

SK範囲によるクエリ

// SKにタイムスタンプを使用している場合、日付範囲内の注文を取得
const items = await dataService.listItemsByPk(
`${tenantCode}${KEY_SEPARATOR}ORDER`,
{
sk: {
skExpression: 'sk BETWEEN :start AND :end',
skAttributeValues: { ':start': startDate, ':end': endDate },
},
},
);

ベストプラクティス

1. 一貫したプレフィックスを使用する

プレフィックスを定数として定義します:

// Good
export const PRODUCT_PK_PREFIX = "PRODUCT";
const pk = `${tenantCode}${KEY_SEPARATOR}${PRODUCT_PK_PREFIX}`;

// Avoid
const pk = `${tenantCode}#PRODUCT`; // Magic string

2. ソート可能なIDにはULIDを使用する

ULIDは一意性と時間ベースのソートの両方を提供します:

import { ulid } from "ulid";

const sk = ulid();
// Result: "01HX7MBJK3V9WQBZ7XNDK5ZT2M"
// 作成時刻でソート可能

3. クエリパターンに合わせた設計

データのクエリ方法に基づいてキーを構造化します:

// 注文の全アイテムを取得する必要がある場合:
PK: tenant001#ORDER
SK: ORDER_ITEM#orderId#itemId // Query by SK prefix

// 注文を横断して商品別にアイテムを取得する必要がある場合:
// GSIまたは別テーブルを検討

4. PKのカーディナリティを管理可能に保つ

ホットパーティションを防ぐために、ユニークなPKが多すぎないようにします:

// 良い - テナントで制限される
PK: tenant001#PRODUCT

// 避ける - ユーザーで制限されない
PK: USER_ACTIVITY#user123 // Could create millions of partitions

5. PKにテナントを含める

マルチテナント分離のために、常にPKにテナントコードを含めます:

// Good
PK: tenant001#PRODUCT

// Avoid
PK: PRODUCT // No tenant isolation
SK: tenant001#productId // Tenant in SK is less efficient
テナントコード正規化 - 破壊的変更

getUserContext()関数はtenantCodeを小文字に正規化します。これはパーティションキー生成に影響します:

// ユーザーのCognitoには大文字のテナントがある
custom:tenant = "MY_TENANT"

// getUserContext()は小文字を返す
tenantCode = "my_tenant"

// 生成されたPKは小文字を使用
PK: my_tenant#PRODUCT

既存データへの影響: 既存のデータが大文字のテナントコードでPKに保存されている場合(例:MY_TENANT#PRODUCT)、正規化されたテナントコードを使用したクエリではそのデータを見つけることができません。

マイグレーションが必要: マイグレーション戦略についてはテナントコード正規化マイグレーションを参照するか、完全なアップグレード手順についてはv1.1.0 マイグレーションガイドを参照してください。

6. 共有データには共通テナントを使用する

テナント間で共有されるデータには共通テナントコードを使用します:

// ユーザーデータ(テナント間で共有)
PK: common#USER
SK: sso#userId

// ユーザー-テナント関連付け
PK: common#USER_TENANT
SK: tenant001#userId

避けるべきアンチパターン

1. キーに情報を詰め込みすぎる

// 避ける - 複雑すぎる
SK: ORDER#2024-01-15#electronics#high-priority#01HX7M...

// 良い - フィルタリングに属性を使用
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M
attributes: {
date: "2024-01-15",
category: "electronics",
priority: "high"
}

2. キーに可変データを使用する

// 避ける - ステータスが変わる
SK: ORDER#pending#01HX7M... // What happens when status changes?

// 良い - 属性を使用
SK: ORDER#01HX7MBJK3V9WQBZ7XNDK5ZT2M
attributes: { status: "pending" }

3. 一貫性のないセパレーター

// 避ける - 混在したセパレーター
SK: ORDER-01HX7M_item:001

// 良い - 一貫したセパレーター
SK: ORDER#01HX7M#ITEM#001

4. データ操作でのバージョンサフィックス

// 避ける - データテーブルSKにバージョンを含める
await dataService.getItem({
pk: "tenant001#PRODUCT",
sk: "01HX7MBJK3V9WQBZ7XNDK5ZT2M@3" // バージョンはここにあるべきではない
});

// 良い - 常にremoveSortKeyVersionを使用
const cleanSk = removeSortKeyVersion(skWithVersion);
await dataService.getItem({ pk, sk: cleanSk });

APIリファレンス

キー関数

関数シグネチャ説明
generateId(pk: string, sk: string) => stringPKとSKをIDに結合し、SKからバージョンを削除
getTenantCode(pk: string) => string | undefinedPKからテナントコードを抽出
addSortKeyVersion(sk: string, version: number) => stringSKにバージョンサフィックスを追加
removeSortKeyVersion(sk: string) => stringSKからバージョンサフィックスを削除
getSortKeyVersion(sk: string) => numberSKからバージョン番号を取得(バージョンがない場合は-1を返す)
masterPk(tenantCode?: string) => stringMASTER#tenantCode PKを生成
seqPk(tenantCode?: string) => stringSEQ#tenantCode PKを生成
ttlSk(tableName: string) => stringTTL#tableName SKを生成

定数

定数使用方法
KEY_SEPARATOR#キーコンポーネントの結合に使用
VER_SEPARATOR@バージョンサフィックスに内部的に使用
VERSION_FIRST0新しいエンティティ作成時に使用
VERSION_LATEST-1SKにバージョンがない場合に返される
TENANT_COMMONcommonクロステナント共有データに使用
DEFAULT_TENANT_CODEsingleシングルテナントモードのデフォルト