データ正規化とは何か
データ正規化は「ネストの深い配列・オブジェクトを、“重複なく参照しやすい形”に整理すること」です。ここが重要です:同じエンティティ(ユーザー、商品、コメントなど)を“1か所だけ”に集約し、他の場所からはID参照にすることで、更新が単純になり、矛盾(片方だけ更新されるなど)を防げます。
// そのままのネスト(重複・矛盾の温床)
const post = {
id: 10,
author: { id: 1, name: "Alice" },
comments: [
{ id: 100, author: { id: 1, name: "Alice" }, text: "Hi" }, // 重複作者
{ id: 101, author: { id: 2, name: "Bob" }, text: "Yo" }
]
};
// 正規化(エンティティを分離、ID参照に)
const normalized = {
entities: {
users: {
1: { id: 1, name: "Alice" },
2: { id: 2, name: "Bob" }
},
comments: {
100: { id: 100, authorId: 1, text: "Hi" },
101: { id: 101, authorId: 2, text: "Yo" }
},
posts: {
10: { id: 10, authorId: 1, commentIds: [100, 101] }
}
},
result: 10 // ルートID
};
JavaScriptなぜ正規化するのか(メリットの核心)
一元更新: 同じユーザーが複数箇所に重複していると、名前変更時に全部書き換えが必要になります。正規化し“users[1]”だけを更新すれば、参照先は自動的に最新になります。
差分検知・パフォーマンス: UI状態では“部分更新”が重要。巨大なネストを毎回触るより、対象のテーブル(例:users)で該当IDだけを非破壊更新するほうが速くて安全です。
参照の明確化: 関係(1対多、多対多)を“IDのリスト”として表すと、結合・検索・集計が直感的になります。DB思考と整合します。
正規化の基本パターン(エンティティと参照)
エンティティごとにテーブル(辞書)化
const entities = {
users: { [id]: userObject },
posts: { [id]: postObject },
comments: { [id]: commentObject }
};
JavaScriptポイント: キーはID(文字列でも数値でも可)。中身は“重複なしの最新レコード”。
関係はID参照へ
// 1対多
posts[10] = { id: 10, authorId: 1, commentIds: [100, 101] };
// 多対多(例:タグ)
tags[1] = { id: 1, name: "news", postIds: [10, 11] };
JavaScriptポイント: 双方向参照は便利だが、更新時の整合(追加・削除を両側へ反映)を設計しておく。
具体的な正規化手順(ネストから辞書を作る)
小さなユーティリティで“抽出と登録”を分ける
function ensure(entities, table, id, record) {
entities[table] ??= {};
entities[table][id] = record;
}
function normalizePost(post) {
const entities = { users: {}, comments: {}, posts: {} };
// author
const author = post.author;
ensure(entities, "users", author.id, author);
// comments
const commentIds = [];
for (const c of post.comments ?? []) {
ensure(entities, "users", c.author.id, c.author);
ensure(entities, "comments", c.id, { id: c.id, authorId: c.author.id, text: c.text });
commentIds.push(c.id);
}
// post
ensure(entities, "posts", post.id, { id: post.id, authorId: author.id, commentIds });
return { entities, result: post.id };
}
JavaScriptここが重要です: “抽出(走査)”と“登録(辞書化)”を分けると読みやすい。欠損(undefined/null)には ?? [] で受け皿を用意し、落ちない実装にする。
逆方向(デノーマライズ):表示用に再構成する
正規化されたデータをUIで表示するには、ID参照を辿って“見やすい形”に戻します。
function denormalizePost(entities, id) {
const post = entities.posts[id];
const author = entities.users[post.authorId];
const comments = post.commentIds.map(cid => {
const c = entities.comments[cid];
return { ...c, author: entities.users[c.authorId] };
});
return { ...post, author, comments };
}
JavaScriptポイント: デノーマライズは“読み取り専用”に寄せる。更新は常に正規化側(辞書)を操作するほうが安全。
更新の設計(イミュータブル+部分更新)
エンティティを直接差し替える(最小更新)
function updateUser(entities, id, patch) {
const prev = entities.users[id] ?? {};
return {
...entities,
users: {
...entities.users,
[id]: { ...prev, ...patch }
}
};
}
JavaScriptここが重要です: 非破壊更新で“該当IDだけ”を作り直す。ネストの深い構造を触らないので差分検知が安定し、バグが減る。
関係の更新(追加・削除の整合)
function addCommentToPost(entities, postId, comment) {
const comments = {
...entities.comments,
[comment.id]: { id: comment.id, authorId: comment.authorId, text: comment.text }
};
const post = entities.posts[postId];
const nextPost = { ...post, commentIds: [...post.commentIds, comment.id] };
return {
...entities,
comments,
posts: { ...entities.posts, [postId]: nextPost }
};
}
JavaScriptポイント: 片方向だけ更新すると“参照の不整合”が起きる。必ず関係の両側(辞書とIDリスト)を一致させる。
正規化の適用判断(いつやるべきか)
小規模ならそのままでもよい: ネストが浅く、更新経路が単純なら過度な正規化は不要。読みやすさを優先。
重複が増え、更新が複雑化したら正規化: 同じユーザーやタグが何箇所にも現れ、更新が複数箇所に波及するようになったら正規化の出番。
サーバーAPIの形に合わせる: APIがネストで返すなら、受け取り後に正規化。送信時は逆に“期待される形”へデノーマライズして返す。
よくある落とし穴と回避策(重要ポイントの深掘り)
IDの一意性を疎かにする: IDが不安定(インデックス依存、生成方法が衝突)だと辞書が壊れる。安定したID設計(サーバー発行やUUID)を使う。
双方向参照の更新漏れ: 追加・削除時に“片側だけ”更新すると、幽霊参照が残る。ユーティリティで両側更新を一括処理する。
過度なデノーマライズ更新: UIで表示用オブジェクトを直接書き換えると辞書と不整合に。常に“辞書(entities)”を更新し、表示は再デノーマライズ。
深いコピーの乱用: 毎回全データを深いコピーすると重い。“該当テーブルの該当IDのみ”を非破壊更新する最小単位に切る。
実践レシピ(すぐ使えるパターン)
ネストレスポンスを一括正規化
function normalizePostsResponse(res) {
const entities = { users: {}, posts: {}, comments: {} };
const results = [];
for (const p of res.posts ?? []) {
ensure(entities, "users", p.author.id, p.author);
const commentIds = [];
for (const c of p.comments ?? []) {
ensure(entities, "users", c.author.id, c.author);
ensure(entities, "comments", c.id, { id: c.id, authorId: c.author.id, text: c.text });
commentIds.push(c.id);
}
ensure(entities, "posts", p.id, { id: p.id, authorId: p.author.id, commentIds });
results.push(p.id);
}
return { entities, results };
}
JavaScript検索・集計は“辞書+ID配列”で効率化
function searchByAuthor(entities, authorId) {
return Object.values(entities.posts)
.filter(p => p.authorId === authorId)
.map(p => denormalizePost(entities, p.id));
}
JavaScript参照整合付き削除(コメントを安全に消す)
function removeComment(entities, postId, commentId) {
const { [commentId]: _, ...nextComments } = entities.comments;
const post = entities.posts[postId];
const nextPost = {
...post,
commentIds: post.commentIds.filter(id => id !== commentId)
};
return {
...entities,
comments: nextComments,
posts: { ...entities.posts, [postId]: nextPost }
};
}
JavaScriptまとめ
データ正規化は「エンティティを辞書化し、関係をID参照にする」ことで、更新を一元化し矛盾を防ぐ設計です。核心は、重複を排除して“該当IDのみ最小非破壊更新”を徹底すること。ネストで受け取り、正規化して保管し、表示時にデノーマライズする流れが実務の定番。IDの安定性、双方向参照の整合、パフォーマンス(部分更新)を押さえれば、初心者でも複雑なネストデータを安全に、明快に運用できます。
