12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

配列の階層構造をデータベースから復元する

Last updated at Posted at 2016-03-23

問題

以下の階層構造を

[
    'name' => 'A',
    'children' => [
        [
            'name' => 'B',
            'children' => [
                [
                    'name' => 'C',
                    'children' => [],
                ],
            ],
        ],
        [
            'name' => 'D',
            'children' => [],
        ]
    ],
]

以下のようなテーブルcategoriesで表現している.

id name parent_id
1 A NULL
2 B 1
3 D 1
4 C 2

このテーブルに対してSQLを発行して,もとの配列の階層構造をできるだけ効率良く復元したい.さてどうしよう?

解決策

再帰クエリを使いましょう.以下の例では,$_GET['id']を受け取ってそこからの階層をJSONで表示します.

<?php

// Content-TypeをUTF-8エンコードされたJSONであるとして明示
header('Content-Type: application/json; charset=UTF-8');

try {

    // データベースに接続
    $pdo = new \PDO('sqlite:example.db', '', '', [
        \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
        \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
    ]);

    // 再帰的に子ノードを取得するSQLを用意
    $stmt = $pdo->prepare("
        WITH RECURSIVE r AS (
            SELECT * FROM categories WHERE id = ?
            UNION ALL
            SELECT categories.* FROM r, categories WHERE r.id = categories.parent_id
        )
        SELECT id, name, parent_id FROM r
    ");

    // $_GET['id'] を確実に整数として「?」にバインドする (未定義の場合はゼロ) 
    $stmt->bindValue(1, (int)filter_input(INPUT_GET, 'id'), PDO::PARAM_INT);

    // SQLを実行
    $stmt->execute();

    // ルートノードを取得
    $row = $stmt->fetch();
    if ($row === false) {
        throw new \RuntimeException('Not Found', 404);
    }
    $root = [
        'name' => $row['name'],
        'children' => [],
    ];
    $map = [$row['id'] => &$root];
    unset($row);

    // その他のノードを配置する
    foreach ($stmt as $row) {
        $node = [
            'name' => $row['name'],
            'children' => [],
        ];
        $map[$row['id']] = &$node;
        $map[$row['parent_id']]['children'][] = &$node;
        unset($row, $node);
    }
    unset($map);

    // 結果を表示
    echo json_encode($root, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

} catch (\PDOException $e) {

    // PDOがスローした例外
    http_response_code(500);
    echo json_encode([
        'error' => $e->getMessage(),
    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

} catch (\RuntimeException $e) {

    // 自分でスローした例外
    http_response_code($e->getCode() ?: 500);
    echo json_encode([
        'error' => $e->getMessage(),
    ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

}
SQLiteデータベース作成用シェルコマンド
echo "
    CREATE TABLE IF NOT EXISTS categories(
        id INTEGER PRIMARY KEY NOT NULL,
        name TEXT NOT NULL,
        parent_id INTEGER
    );
    INSERT INTO categories
        SELECT 1, 'A', NULL
        UNION ALL
        SELECT 2, 'B', 1
        UNION ALL
        SELECT 3, 'D', 1 
        UNION ALL
        SELECT 4, 'C', 2
    ; 
" | sqlite3 example.db

「連想配列の配列」か?「オブジェクトの配列」か?

以下の部分のコードですが…

$root = [
    'name' => $row['name'],
    'children' => [],
];
$map = [$row['id'] => &$root];
unset($row);

foreach ($stmt as $row) {
    $node = [
        'name' => $row['name'],
        'children' => [],
    ];
    $map[$row['id']] = &$node;
    $map[$row['parent_id']]['children'][] = &$node;
    unset($row, $node);
}
unset($map);

もし配列にこだわらずにstdClassを含んでいても問題が無い場合は,おそらくこちらのほうが読みやすくなるでしょう.オブジェクトの場合は配列と違って参照型なので,&を使って**参照代入(参照渡し)**する必要がありません.従って明示的なunsetも要りません.json_encodeする場合はこちらで全く問題無いです.

$root = (object)[
    'name' => $row['name'],
    'children' => [],
];
$map = [$row['id'] => $root];

foreach ($stmt as $row) {
    $map[$row['parent_id']]->children[] = $map[$row['id']] = (object)[
        'name' => $row['name'],
        'children' => [],
    ];
}

そもそも\PDO::FETCH_ASSOC\PDO::FETCH_OBJにする場合はこうなりますね.もっとスッキリします.

$root = (object)[
    'name' => $row->name,
    'children' => [],
];
$map = [$row->id => $root];

foreach ($stmt as $row) {
    $map[$row->parent_id]->children[] = $map[$row->id] = (object)[
        'name' => $row->name,
        'children' => [],
    ];
}

余談ですが,PHP7からarray_columnが微妙に強化されています.

スクリーンショット 2016-03-24 16.27.48.png
スクリーンショット 2016-03-24 16.27.37.png

これにより,今後この関数を使いたい場合においても,「連想配列の配列」に拘る必要は無くなっていくと思われます.「オブジェクトの配列」のほうがよりシンプルに書けますしね.

連想配列の配列
$first_child_of_first_child = $node['children'][0]['children'][0];
$children_of_children = array_column($node['children'], 'children'); // PHP5.5+
オブジェクトの配列
$first_child_of_first_child = $node->children[0]->children[0];
$children_of_children = array_column($node->children, 'children'); // PHP7.0+

PHP5を切り捨ててもいい場合は,json_decodeする場合も第2引数に思考停止trueを渡すのは避けるようにしていきましょう.

12
12
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?