JavaScript | 配列・オブジェクト:ネスト構造の扱い – データ正規化

JavaScript JavaScript
スポンサーリンク

データ正規化とは何か

データ正規化は「ネストの深い配列・オブジェクトを、“重複なく参照しやすい形”に整理すること」です。ここが重要です:同じエンティティ(ユーザー、商品、コメントなど)を“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の安定性、双方向参照の整合、パフォーマンス(部分更新)を押さえれば、初心者でも複雑なネストデータを安全に、明快に運用できます。

タイトルとURLをコピーしました