sql >> Base de Datos >  >> RDS >> PostgreSQL

Postgres:convierte la lista de adyacencia en un objeto JSON anidado

Usando WITH RECURSIVE (https://www.postgresql.org/docs/current/static/queries-with.html) y Funciones JSON (https://www.postgresql.org/docs/current/static/functions-json.html) I construye esta solución:

db<>violín

La funcionalidad principal:

    WITH RECURSIVE tree(node_id, ancestor, child, path, json) AS  (
      SELECT 
          t1.node_id, 
          NULL::int, 
          t2.node_id,
          '{children}'::text[] || 
             (row_number() OVER (PARTITION BY t1.node_id ORDER BY t2.node_id) - 1)::text,-- C
          jsonb_build_object('name', t2.name, 'children', array_to_json(ARRAY[]::int[])) -- B
      FROM test t1
      LEFT JOIN test t2 ON t1.node_id = t2.parent_node                                   -- A
      WHERE t1.parent_node IS NULL

      UNION

      SELECT
          t1.node_id, 
          t1.parent_node, 
          t2.node_id,
          tree.path || '{children}' || (row_number() OVER (PARTITION BY t1.node_id ORDER BY t2.node_id) - 1)::text, 
          jsonb_build_object('name', t2.name, 'children', array_to_json(ARRAY[]::int[]))
      FROM test t1
      LEFT JOIN test t2 ON t1.node_id = t2.parent_node
      INNER JOIN tree ON (t1.node_id = tree.child)
      WHERE t1.parent_node = tree.node_id                                                -- D
    )
    SELECT                                                                               -- E
        child as node_id, path, json 
    FROM tree 
    WHERE child IS NOT NULL ORDER BY path

Cada WITH RECURSIVE contiene un inicio SELECT y una parte recursiva (la segunda SELECT ) combinado por un UNION .

R:Unirse a la tabla contra sí misma para encontrar los hijos de un node_id .

B:Construyendo el objeto json para el niño que se puede insertar en su padre

C:Construyendo la ruta donde se debe insertar el objeto secundario (desde la raíz). La función de ventana row_number() (https://www.postgresql.org/docs/current/static/tutorial-window.html) genera el índice del elemento secundario dentro de la matriz de elementos secundarios del elemento principal.

D:La parte de recursión funciona como la parte inicial con una diferencia:no busca el elemento raíz sino el elemento que tiene el nodo principal de la última recursión.

E:Ejecutar la recursividad y filtrar todos los elementos sin hijos da este resultado:

node_id   path                      json
2         children,0                {"name": "node2", "children": []}
4         children,0,children,0     {"name": "node4", "children": []}
5         children,0,children,1     {"name": "node5", "children": []}
6         children,0,children,2     {"name": "node6", "children": []}
3         children,1                {"name": "node3", "children": []}
7         children,1,children,0     {"name": "node7", "children": []}
8         children,1,children,1     {"name": "node8", "children": []}

Aunque no encontré la manera de agregar todos los elementos secundarios en la recursividad (el json de origen no es una variable global, por lo que siempre conoce los cambios de los ancestros directos, no de sus hermanos), tuve que iterar las filas en un paso de segundos.

Es por eso que construyo la función. Allí puedo hacer la iteración de una variable global. Con la función jsonb_insert Estoy insertando todos los elementos calculados en un objeto json raíz, usando la ruta calculada.

CREATE OR REPLACE FUNCTION json_tree() RETURNS jsonb AS $$
DECLARE
    _json_output jsonb;
    _temprow record;
BEGIN
    SELECT 
        jsonb_build_object('name', name, 'children', array_to_json(ARRAY[]::int[])) 
    INTO _json_output 
    FROM test 
    WHERE parent_node IS NULL;

    FOR _temprow IN
        /* Query above */
    LOOP
        SELECT jsonb_insert(_json_output, _temprow.path, _temprow.json) INTO _json_output;
    END LOOP;

    RETURN _json_output;
END;
$$ LANGUAGE plpgsql;

El último paso es llamar a la función y hacer que el JSON sea más legible (jsonb_pretty() )

{
    "name": "node1",
    "children": [{
        "name": "node2",
        "children": [{
            "name": "node4",
            "children": []
        },
        {
            "name": "node5",
            "children": []
        },
        {
            "name": "node6",
            "children": []
        }]
    },
    {
        "name": "node3",
        "children": [{
            "name": "node7",
            "children": []
        },
        {
            "name": "node8",
            "children": []
        }]
    }]
}

Estoy seguro de que es posible optimizar la consulta, pero para un boceto funciona.