Edited at

[TypeScript]TypeORMで簡単に作るDBの効率的なツリー構造


[TypeScript]TypeORMで簡単に作るDBの効率的なツリー構造

 TypeORMを利用すると、データベース上にツリー構造を容易に構築することが出来ます。最善のロジックでツリー構造を扱うため、階層ごとにselectするような無駄な動作を避けることが出来ます。

 ツリーを扱うためのアルゴリズムはデコレータのパラメータで選択することが可能です


  • Nested set

  • Materialized Path

  • Closure table

 などから選ぶことが出来ます。今回は単一テーブルで扱え、書き込みが遅く、読み込みが早いという特徴を持つNested setを使用しています。

 実行結果を見てもらえれば分かりますが、発行しているSQL文は基準位置のIDを検出一回とデータの取得一回、計たったの二回です。これで子ノードや親ノードを全て取得することが可能なのです。

 とても有用なのですが、欠点もあります。ツリー構造でデータを取得すると、標準で用意されている命令では抽出フィールドを選択することが出来ません。また、起点IDをサブクエリー化すれば、クエリーの呼び出し回数を減らせるのですが、ツリーを取り扱う標準機能では対応していません。やろうとすると機能の再実装に近い形になるので、拡張方法は別記事を書く予定です。


ソースコード

import * as typeorm from "typeorm"; 

/**
*TypeORM用データ構造
*
* @export
* @class ItemEntity
*/

@typeorm.Entity()
@typeorm.Tree("nested-set") //書き込みが遅く読み込みが早い、単一テーブルの構造
export class ItemEntity {
constructor(name: string, parent?: ItemEntity) {
this.name = name;
this.parent = parent;
}
@typeorm.PrimaryGeneratedColumn() //自動番号
id!: number;

@typeorm.Column()
name: string;

@typeorm.TreeChildren() //子階層(テーブルにはデータが作られない)
children?: ItemEntity[];

@typeorm.TreeParent() //親(実際にはparentId:numberというフィールドが作られる)
parent?: ItemEntity;
}

//サンプルデータ構造
interface ItemValue {
name: string;
children?: ItemValue[];
}
//サンプルデータ
const itemValue: ItemValue = {
name: "ROOT",
children: [
{
name: "武器",
children: [
{ name: "", children: [{ name: "銅の剣" }, { name: "鉄の剣" }] },
{
name: "",
children: [{ name: "檜の棒" }, { name: "棍棒" }, { name: "金棒" }]
}
]
},
{
name: "防具",
children: [
{ name: "", children: [{ name: "布の服" }, { name: "革の鎧" }] },
{
name: "",
children: [{ name: "鍋の蓋" }, { name: "皮の盾" }, { name: "鉄の盾" }]
}
]
}
]
};

/**
*非同期主処理
*
*/

async function Main() {
//DBへ接続
const con = await typeorm.createConnection({
type: "sqlite",
database: `test2.sqlite`,
entities: [ItemEntity],
logging: true,
synchronize: true
});

//Tree操作用のRepositoryを取得
//(getRepositoryでも実は返ってくるのは同じインスタンス)
const rep = con.getTreeRepository(ItemEntity);

//データが無ければサンプルデータをDB上に作成
if ((await rep.count()) === 0) {
const saveItem = async (item: ItemValue, parent?: ItemEntity) => {
const itemEntity = new ItemEntity(item.name, parent);
await rep.save(itemEntity);
if (item.children)
for (const child of item.children) await saveItem(child, itemEntity);
};
await saveItem(itemValue);
}

//全てのデータが通常通り列挙される
console.log("--- データの取得1 普通に列挙 ----");
const item01 = await rep.find();
console.log(JSON.stringify(item01, null, " "));

//ツリー構造を構築して全データを返す
console.log("--- データの取得2 全ツリー----");
const item02 = await rep.findTrees();
console.log(JSON.stringify(item02, null, " "));

//親方向へ階層が構築される
console.log("--- データの取得3 親をたどる----");
const item03 = await rep.findAncestorsTree((await rep.findOne({
name: "金棒"
})) as ItemEntity);
console.log(JSON.stringify(item03, null, " "));

//子方向へツリー構造を構築する
console.log("--- データの取得4 子をたどる----");
const item04 = await rep.findDescendantsTree((await rep.findOne(
{ name: "防具" },
{ select: ["id"] }
)) as ItemEntity);
console.log(JSON.stringify(item04, null, " "));

await con.close();
}

//主処理の呼び出し
Main();


出力結果


 データの取得1 普通に列挙

データが直列に出力されるだけです

生成SQL


query: SELECT "ItemEntity"."id" AS "ItemEntity_id", "ItemEntity"."name" AS "ItemEntity_name", "ItemEntity"."nsleft" AS "ItemEntity_nsleft", "ItemEntity"."nsright" AS "ItemEntity_nsright", "ItemEntity"."parentId" AS "ItemEntity_parentId" FROM "item_entity" "ItemEntity"

出力結果のJSON

[

{
"name": "ROOT",
"id": 1
},
{
"name": "武器",
"id": 2
},
{
"name": "剣",
"id": 3
},
{
"name": "銅の剣",
"id": 4
},
{
"name": "鉄の剣",
"id": 5
},
{
"name": "棒",
"id": 6
},
{
"name": "檜の棒",
"id": 7
},
{
"name": "棍棒",
"id": 8
},
{
"name": "金棒",
"id": 9
},
{
"name": "防具",
"id": 10
},
{
"name": "鎧",
"id": 11
},
{
"name": "布の服",
"id": 12
},
{
"name": "革の鎧",
"id": 13
},
{
"name": "盾",
"id": 14
},
{
"name": "鍋の蓋",
"id": 15
},
{
"name": "皮の盾",
"id": 16
},
{
"name": "鉄の盾",
"id": 17
}
]


 データの取得2 全ツリー

 親子関係が自動的に構築されます

生成SQL

query: SELECT "treeEntity"."id" AS "treeEntity_id", "treeEntity"."name" AS "treeEntity_name", "treeEntity"."nsleft" AS "treeEntity_nsleft", "treeEntity"."nsright" AS "treeEntity_nsright", "treeEntity"."parentId" AS "treeEntity_parentId" FROM "item_entity" "treeEntity" WHERE "treeEntity"."parentId" IS NULL 

query: SELECT "treeEntity"."id" AS "treeEntity_id", "treeEntity"."name" AS "treeEntity_name", "treeEntity"."nsleft" AS "treeEntity_nsleft", "treeEntity"."nsright" AS "treeEntity_nsright", "treeEntity"."parentId" AS "treeEntity_parentId" FROM "item_entity" "treeEntity" INNER JOIN "item_entity" "joined" ON "treeEntity"."nsleft" BETWEEN "joined"."nsleft" AND "joined"."nsright" WHERE "joined"."id" = ? -- PARAMETERS: [1]

出力結果のJSON

[

{
"name": "ROOT",
"id": 1,
"children": [
{
"name": "武器",
"id": 2,
"children": [
{
"name": "剣",
"id": 3,
"children": [
{
"name": "銅の剣",
"id": 4,
"children": []
},
{
"name": "鉄の剣",
"id": 5,
"children": []
}
]
},
{
"name": "棒",
"id": 6,
"children": [
{
"name": "檜の棒",
"id": 7,
"children": []
},
{
"name": "棍棒",
"id": 8,
"children": []
},
{
"name": "金棒",
"id": 9,
"children": []
}
]
}
]
},
{
"name": "防具",
"id": 10,
"children": [
{
"name": "鎧",
"id": 11,
"children": [
{
"name": "布の服",
"id": 12,
"children": []
},
{
"name": "革の鎧",
"id": 13,
"children": []
}
]
},
{
"name": "盾",
"id": 14,
"children": [
{
"name": "鍋の蓋",
"id": 15,
"children": []
},
{
"name": "皮の盾",
"id": 16,
"children": []
},
{
"name": "鉄の盾",
"id": 17,
"children": []
}
]
}
]
}
]
}
]


 データの取得3 親をたどる

 親方向に向かってデータを取り出します

 起点のIDが分かっていれば、クエリは一回で可能です

 また、起点IDの取得をサブクエリー化する機能は標準では用意されていません。

 もしやるなら、それなりの再実装が必要となります。

生成SQL

query: SELECT "ItemEntity"."id" AS "ItemEntity_id", "ItemEntity"."name" AS "ItemEntity_name", "ItemEntity"."nsleft" AS "ItemEntity_nsleft", "ItemEntity"."nsright" AS "ItemEntity_nsright", "ItemEntity"."parentId" AS "ItemEntity_parentId" FROM "item_entity" "ItemEntity" WHERE "ItemEntity"."name" = ? LIMIT 1 -- PARAMETERS: ["金棒"] 

query: SELECT "treeEntity"."id" AS "treeEntity_id", "treeEntity"."name" AS "treeEntity_name", "treeEntity"."nsleft" AS "treeEntity_nsleft", "treeEntity"."nsright" AS "treeEntity_nsright", "treeEntity"."parentId" AS "treeEntity_parentId" FROM "item_entity" "treeEntity" INNER JOIN "item_entity" "joined" ON "joined"."nsleft" BETWEEN "treeEntity"."nsleft" AND "treeEntity"."nsright" WHERE "joined"."id" = ? -- PARAMETERS: [9]

出力結果のJSON

{

"name": "金棒",
"parent": {
"name": "棒",
"parent": {
"name": "武器",
"parent": {
"name": "ROOT",
"id": 1
},
"id": 2
},
"id": 6
},
"id": 9
}


 データの取得4 子をたどる

 子方向へ向かってデータを取得します

生成SQL

query: SELECT "ItemEntity"."id" AS "ItemEntity_id" FROM "item_entity" "ItemEntity" WHERE "ItemEntity"."name" = ? LIMIT 1 -- PARAMETERS: ["防具"] 

query: SELECT "treeEntity"."id" AS "treeEntity_id", "treeEntity"."name" AS "treeEntity_name", "treeEntity"."nsleft" AS "treeEntity_nsleft", "treeEntity"."nsright" AS "treeEntity_nsright", "treeEntity"."parentId" AS "treeEntity_parentId" FROM "item_entity" "treeEntity" INNER JOIN "item_entity" "joined" ON "treeEntity"."nsleft" BETWEEN "joined"."nsleft" AND "joined"."nsright" WHERE "joined"."id" = ? -- PARAMETERS: [10]

出力結果のJSON

{

"id": 10,
"children": [
{
"name": "鎧",
"id": 11,
"children": [
{
"name": "布の服",
"id": 12,
"children": []
},
{
"name": "革の鎧",
"id": 13,
"children": []
}
]
},
{
"name": "盾",
"id": 14,
"children": [
{
"name": "鍋の蓋",
"id": 15,
"children": []
},
{
"name": "皮の盾",
"id": 16,
"children": []
},
{
"name": "鉄の盾",
"id": 17,
"children": []
}
]
}
]
}


まとめ

 とにかくDBで効率よくツリー構造を作りたい、そんな用途にはもってこいです。ただ、この辺りを解説している情報が公式ドキュメント以外ほぼ皆無なので、ちょっと機能を付け足したいと言うような場合は、元ソースと睨めっこすることになります。

 とにかく便利なので、とてもオススメです。