JavaScript
 Computer >> コンピューター >  >> プログラミング >> JavaScript

JavaScriptでDB形式のフラットデータをJSONツリー(ネスト構造)に変換する方法

はじめに

データベースでは、カテゴリ一覧や組織図、地域マスタのような階層データを、各レコードが parentId で親を参照するフラットな配列(隣接リスト形式)として保存するのが一般的です。しかし、フロントエンドでツリービューを描画したり、再帰的にデータを処理したりする場面では、親子関係がネストされた JSON 構造へ変換する必要があります。

本記事では、JavaScript だけでこの変換を行うシンプルかつ高速な手法を、具体的なコード例とともに解説します。

入力データ(DB形式)

まず、変換元となる配列を見てみましょう。各オブジェクトは id、name、親ノードの ID を表す parentId を持ち、ルートノードのみ parentId が null になっています。

const arr = [
  { "id": 7,  "name": "Kuwait",             "parentId": 2 },
  { "id": 4,  "name": "Iraq",               "parentId": 2 },
  { "id": 10, "name": "Qatar",              "parentId": 2 },
  { "id": 2,  "name": "Middle East",        "parentId": 1 },
  { "id": 3,  "name": "Bahrain",            "parentId": 2 },
  { "id": 6,  "name": "Jordan",             "parentId": 2 },
  { "id": 8,  "name": "Lebanon",            "parentId": 2 },
  { "id": 1,  "name": "Africa/Middle East", "parentId": null },
  { "id": 5,  "name": "Israel",             "parentId": 2 },
  { "id": 9,  "name": "Oman",               "parentId": 2 }
];

ポイントは、「Africa/Middle East」(id: 1)だけがトップレベル(ルート)であり、残りの国々はすべて「Middle East」(id: 2)の子ノードだという点です。

期待される出力(JSON形式)

求めるのは、親ノードの children プロパティの中に子オブジェクトが入れ子になった、次のようなツリー構造です。

[
  {
    "id": 1,
    "name": "Africa/Middle East",
    "parentId": null,
    "children": [
      {
        "id": 2,
        "name": "Middle East",
        "parentId": 1,
        "children": [
          { "id": 7,  "name": "Kuwait",  "parentId": 2, "children": [] },
          { "id": 4,  "name": "Iraq",    "parentId": 2, "children": [] },
          { "id": 10, "name": "Qatar",   "parentId": 2, "children": [] },
          { "id": 3,  "name": "Bahrain", "parentId": 2, "children": [] },
          { "id": 6,  "name": "Jordan",  "parentId": 2, "children": [] },
          { "id": 8,  "name": "Lebanon", "parentId": 2, "children": [] },
          { "id": 5,  "name": "Israel",  "parentId": 2, "children": [] },
          { "id": 9,  "name": "Oman",    "parentId": 2, "children": [] }
        ]
      }
    ]
  }
]

変換関数の実装

この変換には、ハッシュマップ(id をキーとした辞書)を活用するのが定番のアプローチです。配列を一度だけ走査しながら、各ノードをマップに登録し、同時に親の children 配列へ自分自身を追加していくことで、計算量 O(n) でツリーを組み立てられます。

コード例

const transformTree = (data, root = null) => {
  const res = [];  // 結果となるツリー(ルートノードの配列)
  const map = {};  // id をキーにノードを素早く参照するためのマップ

  data.forEach((el) => {
    // すでに子が登録されていれば引き継ぎ、なければ空配列を設定
    el.children = (map[el.id] && map[el.id].children) || [];
    map[el.id] = el;

    if (el.parentId === root) {
      // parentId が null(=ルート)なら結果のトップレベルへ
      res.push(el);
    } else {
      // 親がまだ出現していなくても、プレースホルダーを先に用意
      map[el.parentId] = map[el.parentId] || {};
      map[el.parentId].children = map[el.parentId].children || [];
      map[el.parentId].children.push(el);
    }
  });

  return res;
};

console.log(JSON.stringify(transformTree(arr), undefined, 4));

仕組みのポイント

  • O(n) の高速処理: map によって任意のノードへ id 一発でアクセスできるため、二重ループを使わず線形時間で処理が完了します。
  • 入力順序に依存しない: 親ノードが子よりも後に出現する場合でも、空オブジェクトと children 配列を先に確保しておくため正しく紐づきます。親が後に map へ登録される際、既存の children がそのまま引き継がれる仕組みです。
  • 柔軟なルート指定: 第2引数 root の値を変更すれば、任意のノードを起点とした部分ツリーも簡単に取得できます。

実行結果

スクリプトを実行すると、コンソールには次のように整形された JSON ツリーが出力されます。

[
    {
        "id": 1,
        "name": "Africa/Middle East",
        "parentId": null,
        "children": [
            {
                "id": 2,
                "name": "Middle East",
                "parentId": 1,
                "children": [
                    {
                        "id": 7,
                        "name": "Kuwait",
                        "parentId": 2,
                        "children": []
                    },
                    {
                        "id": 4,
                        "name": "Iraq",
                        "parentId": 2,
                        "children": []
                    },
                    {
                        "id": 10,
                        "name": "Qatar",
                        "parentId": 2,
                        "children": []
                    },
                    {
                        "id": 3,
                        "name": "Bahrain",
                        "parentId": 2,
                        "children": []
                    },
                    {
                        "id": 6,
                        "name": "Jordan",
                        "parentId": 2,
                        "children": []
                    },
                    {
                        "id": 8,
                        "name": "Lebanon",
                        "parentId": 2,
                        "children": []
                    },
                    {
                        "id": 5,
                        "name": "Israel",
                        "parentId": 2,
                        "children": []
                    },
                    {
                        "id": 9,
                        "name": "Oman",
                        "parentId": 2,
                        "children": []
                    }
                ]
            }
        ]
    }
]

まとめ

DB 形式(parentId 参照のフラットな配列)のデータを JSON のネスト構造へ変換するには、id をキーとしたマップを併用して配列を一度だけ走査する方法が最も効率的です。大量データでも高速に動作し、入力の並び順にも左右されない堅牢な実装のため、カテゴリ一覧、組織図、コメントスレッドなど、さまざまな階層データの表示処理に幅広く応用できます。

  1. JavaScriptのJSON配列とは?基本構造と実装例をわかりやすく解説

    JSON(JavaScript Object Notation)は軽量なデータ交換形式であり、その配列はJavaScriptの配列とほぼ同じ形で表現されます。角括弧 [] の中に値をカンマ区切りで並べることで、文字列や数値、オブジェクトなど複数のデータをひとつにまとめて扱えます。以下は、JavaScriptのオブジェクト内にJSON配列を含めた基本的な例です。let obj = { name:Rohan, sports : [cricket,Football,volleyball,hockey] }この例では、「sports」というプロパティに4つのスポーツ名が配列として格納さ

  2. JavaScriptでJSON配列からデータを読み取る方法をサンプルコード付きで解説

    Web開発では、APIから取得したJSONデータを処理する場面が非常によくあります。本記事では、JavaScriptを使ってJSON配列からデータを読み取り、画面に表示する方法を、実際に動作するサンプルコードとともにわかりやすく解説します。 JSON配列からデータを読み取る基本の流れ JSON形式の文字列をJavaScriptで扱うには、まずJSON.parse()メソッドで文字列をJavaScriptのオブジェクト(配列)に変換します。変換後は、forEach()メソッドなどを活用して、配列の各要素に格納された値へ簡単にアクセスできます。 サンプルコード <!DOCTYPE htm