배열의 계층 구조를 데이터베이스에서 복원

문제



다음 계층 구조
[
    'name' => 'A',
    'children' => [
        [
            'name' => 'B',
            'children' => [
                [
                    'name' => 'C',
                    'children' => [],
                ],
            ],
        ],
        [
            'name' => 'D',
            'children' => [],
        ]
    ],
]

다음과 같은 테이블 categories로 표현된다.


id
이름
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가 미묘하게 강화되었습니다.




이것에 의해, 향후 이 함수를 사용하고 싶은 경우에 있어서도, 「연상 배열의 배열」에 구애할 필요는 없어져 갈 것이라고 생각됩니다. 「오브젝트의 배열」쪽이 보다 심플하게 쓸 수 있군요.

연관 배열의 배열
$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 할 때도 두 번째 인수에 사고 정지 true를 넘기는 것은 피하도록 합시다.

좋은 웹페이지 즐겨찾기