Cómo crear jerarquías de objetos Java a partir de listas planas con Collector

Cómo crear jerarquías de objetos Java a partir de listas planas con Collector

A veces desea escribir una consulta SQL y recuperar una jerarquía de datos, cuya representación plana podría verse así:

SELECT id, parent_id, label
FROM t_directory;

El resultado podría ser:

|id |parent_id|label              |
|---|---------|-------------------|
|1  |         |C:                 |
|2  |1        |eclipse            |
|3  |2        |configuration      |
|4  |2        |dropins            |
|5  |2        |features           |
|7  |2        |plugins            |
|8  |2        |readme             |
|9  |8        |readme_eclipse.html|
|10 |2        |src                |
|11 |2        |eclipse.exe        |
Índice
  1. Obtener jerarquía con SQL
  2. Haz esto con jOOQ 3.19
  3. ¿No usas jOOQ? No hay problema, simplemente copie este recopilador:
  4. Un ejemplo jOOQ más complejo
  5. Conclusión
    1. Así:

Obtener jerarquía con SQL

Ahora puede ejecutar una consulta PostgreSQL recursiva como el monstruo a continuación para convertirlo en un documento JSON:

WITH RECURSIVE
  d1 (id, parent_id, name) as (
    SELECT id, parent_id, label
    FROM t_directory
  ),
  d2 AS (
    SELECT d1.*, 0 AS level
    FROM d1
    WHERE parent_id IS NULL
    UNION ALL
    SELECT d1.*, d2.level + 1
    FROM d1
    JOIN d2 ON d2.id = d1.parent_id
  ),
  d3 AS (
    SELECT d2.*, jsonb_build_array() children
    FROM d2
    WHERE level = (SELECT max(level) FROM d2)
    UNION (
      SELECT (branch_parent).*, jsonb_agg(branch_child)
      FROM (
        SELECT 
          branch_parent, 
          to_jsonb(branch_child) - 'level' - 'parent_id' AS branch_child
        FROM d2 branch_parent
        JOIN d3 branch_child ON branch_child.parent_id = branch_parent.id
      ) branch
      GROUP BY branch.branch_parent
      UNION
      SELECT d2.*, jsonb_build_array()
      FROM d2
      WHERE d2.id NOT IN (
        SELECT parent_id FROM d2 WHERE parent_id IS NOT NULL
      )
    )
  )
SELECT
  jsonb_pretty(jsonb_agg(to_jsonb(d3) - 'level' - 'parent_id')) AS tree
FROM d3
WHERE level = 0;

También di esta consulta en respuesta a esta pregunta de desbordamiento de pila. Algunas inspiraciones para la consulta en esta publicación de blog.

Y he aquí, tenemos un árbol JSON:

[
    {
        "id": 1,
        "name": "C:",
        "children": [
            {
                "id": 2,
                "name": "eclipse",
                "children": [
                    {
                        "id": 7,
                        "name": "plugins"
                    },
                    {
                        "id": 4,
                        "name": "dropins"
                    },
                    {
                        "id": 8,
                        "name": "readme",
                        "children": [
                            {
                                "id": 9,
                                "name": "readme_eclipse.html"
                            }
                        ]
                    },
                    {
                        "id": 11,
                        "name": "eclipse.exe"
                    },
                    {
                        "id": 10,
                        "name": "src"
                    },
                    {
                        "id": 5,
                        "name": "features"
                    },
                    {
                        "id": 3,
                        "name": "configuration"
                    }
                ]
            }
        ]
    }
]

Pero esa es una consulta SQL bastante bestia, y tal vez no necesite hacerlo con SQL en primer lugar.

Haz esto con jOOQ 3.19

De hecho, comenzando con jOOQ 3.19 y #12341, puede hacer esto completamente con jOOQ, usando un Collector.

Suponiendo que tiene esta representación del lado del cliente para sus datos:

record File(int id, String name, List<File> children) {}

Ahora puedes escribir:

List<File> result =
ctx.select(T_DIRECTORY.ID, T_DIRECTORY.PARENT_ID, T_DIRECTORY.LABEL)
   .from(T_DIRECTORY)
   .orderBy(T_DIRECTORY.ID)
   .collect(Records.intoHierarchy(
       r -> r.value1(),
       r -> r.value2(),
       (r, l) -> new File(r.value1(), r.value3(), l)
   ));

Tenga en cuenta que dependiendo de qué tan fuerte funcione la inferencia de tipo a su favor o no, es posible que deba señalar los tipos de (e, l) -> ... lambda

¡Eso es! Cuando imprimes el resultado, obtienes:

[
  File[id=1, name=C:, children=[
    File[id=2, name=eclipse, children=[
      File[id=3, name=configuration, children=[]], 
      File[id=4, name=dropins, children=[]], 
      File[id=5, name=features, children=[]], 
      File[id=7, name=plugins, children=[]], 
      File[id=8, name=readme, children=[
        File[id=9, name=readme_eclipse.html, children=[]]
      ]], 
      File[id=10, name=src, children=[]], 
      File[id=11, name=eclipse.exe, children=[]]
    ]]
  ]]
]

O, si prefiere la salida JSON, simplemente use Jackson, o lo que sea, para serializar sus datos de esta manera:

new ObjectMapper()
    .writerWithDefaultPrettyPrinter()
    .writeValue(System.out, result);

Y ahora obtienes:

[ {
  "id" : 1,
  "name" : "C:",
  "children" : [ {
    "id" : 2,
    "name" : "eclipse",
    "children" : [ {
      "id" : 3,
      "name" : "configuration"
    }, {
      "id" : 4,
      "name" : "dropins"
    }, {
      "id" : 5,
      "name" : "features"
    }, {
      "id" : 7,
      "name" : "plugins"
    }, {
      "id" : 8,
      "name" : "readme",
      "children" : [ {
        "id" : 9,
        "name" : "readme_eclipse.html"
      } ]
    }, {
      "id" : 10,
      "name" : "src"
    }, {
      "id" : 11,
      "name" : "eclipse.exe"
    } ]
  } ]
} ]

Muy bueno, ¿eh?

¿No usas jOOQ? No hay problema, simplemente copie este recopilador:

Lo anterior no es realmente magia específica de jOOQ. Puedes copiar lo siguiente Collector de jOOQ para lograr lo mismo con su código Java puro:

// Possibly, capture the List<E> type in a new type variable in case you 
// have trouble with type inference
public static final <K, E, R> Collector<R, ?, List<E>> intoHierarchy(
    Function<? super R, ? extends K> keyMapper,
    Function<? super R, ? extends K> parentKeyMapper,
    BiFunction<? super R, ? super List<E>, ? extends E> recordMapper
) {
    return intoHierarchy(
        keyMapper, parentKeyMapper, recordMapper, ArrayList::new
    );
}

public static final <
    K, E, C extends Collection<E>, R
> Collector<R, ?, List<E>> intoHierarchy(
    Function<? super R, ? extends K> keyMapper,
    Function<? super R, ? extends K> parentKeyMapper,
    BiFunction<? super R, ? super C, ? extends E> recordMapper,
    Supplier<? extends C> collectionFactory
) {
    record Tuple3<T1, T2, T3>(T1 t1, T2 t2, T3 t3) {}
    return Collectors.collectingAndThen(
        Collectors.toMap(keyMapper, r -> {
            C e = collectionFactory.get();
            return new Tuple3<R, C, E>(r, e, recordMapper.apply(r, e));
        }),
        m -> {
            List<E> r = new ArrayList<>();

            m.forEach((k, v) -> {
                K parent = parentKeyMapper.apply(v.t1());
                E child = v.t3();

                if (m.containsKey(parent))
                    m.get(parent).t2().add(child);
                else
                    r.add(child);
            });

            return r;
        }
    );
}

Con este colector, y los siguientes tipos/datos:

record Flat(int id, int parentId, String name) {}
record Hierarchical(int id, String name, List<Hierarchical> children) {}

List<Flat> data = List.of(
    new Flat(1, 0, "C:"),
    new Flat(2, 1, "eclipse"),
    new Flat(3, 2, "configuration"),
    new Flat(4, 2, "dropins"),
    new Flat(5, 2, "features"),
    new Flat(7, 2, "plugins"),
    new Flat(8, 2, "readme"),
    new Flat(9, 8, "readme_eclipse.html"),
    new Flat(10, 2, "src"),
    new Flat(11, 2, "eclipse.exe")
);

Ahora puede recrear la misma jerarquía usando el Collector directamente en la lista:

List<Hierarchical> result =
data.stream().collect(intoHierarchy(
    e -> e.id(),
    e -> e.parentId(),
    (e, l) -> new Hierarchical(e.id(), e.name(), l)
));

Tenga en cuenta que dependiendo de qué tan fuerte funcione la inferencia de tipos a su favor o no, es posible que deba volver a indicar los tipos (e, l) -> ... lambda

Un ejemplo jOOQ más complejo

En jOOQ, todos los resultados, incluidas las colecciones anidadas (por ejemplo, las producidas por MULTISET) se pueden recopilar, por lo que si tiene una jerarquía anidada, como comentarios en una publicación de blog, simplemente recopile con jOOQ.

Suponiendo este patrón:

CREATE TABLE post (
  id INT PRIMARY KEY,
  title TEXT
);

CREATE TABLE comment (
  id INT PRIMARY KEY,
  parent_id INT REFERENCES comment,
  post_id INT REFERENCES post,
  text TEXT
);

INSERT INTO post 
VALUES
  (1, 'Helo'),
  (2, 'World');
  
INSERT INTO comment
VALUES 
  (1, NULL, 1, 'You misspelled "Hello"'),
  (2, 1, 1, 'Thanks, will fix soon'),
  (3, 2, 1, 'Still not fixed'),
  (4, NULL, 2, 'Impeccable blog post, thanks');

Podrías escribir una consulta como esta:

record Post(int id, String title, List<Comment> comments) {}
record Comment(int id, String text, List<Comment> replies) {}

List<Post> result =
ctx.select(
       POST.ID, 
       POST.TITLE,
       multiset(
           select(COMMENT.ID, COMMENT.PARENT_ID, COMMENT.TEXT)
           .from(COMMENT)
           .where(COMMENT.POST_ID.eq(POST.ID))
       ).convertFrom(r -> r.collect(intoHierarchy(
           r -> r.value1(),
           r -> r.value2(),
           (e, l) -> new Comment(r.value1(), r.value3(), l)
       ))
   )
   .from(POST)
   .orderBy(POST.ID)
   .fetch(mapping(Post::new));

¡Todo es seguro, como siempre con jOOQ!

Ahora mire lo que esto imprime, cuando se serializa con Jackson:

[ {
  "id" : 1,
  "title" : "Helo",
  "comments" : [ {
    "id" : 1,
    "text" : "You misspelled \"Hello\"",
    "replies" : [ {
      "id" : 2,
      "text" : "Thanks, will fix soon",
      "replies" : [ {
        "id" : 3,
        "text" : "Still not fixed"
      } ]
    } ]
  } ]
}, {
  "id" : 2,
  "title" : "World",
  "comments" : [ {
    "id" : 4,
    "text" : "Impeccable blog post, thanks"
  } ]
} ]

Tenga en cuenta que si solo desea mostrar un subárbol o un árbol hasta una cierta profundidad, siempre puede ejecutar una consulta jerárquica en su MULTISET subconsulta usando WITH RECURSIVE O CONNECT BY.

Conclusión

Collector es una API muy subestimada en el JDK. Cualquier JDK Collection se puede transformar en un Stream y sus elementos pueden ser coleccionados. En jOOQ, un ResultQuery es un Iterableque también ofrece práctica collect() método (simplemente ejecuta la consulta, transmite los resultados y recopila los registros en su Collector).

Nuestra biblioteca funcional jOOλ tiene muchas variedades adicionales en su Agg clase, por ejemplo para:

  • Agregación bit a bit
  • Agregación estadística, como desviación estándar, correlación, percentiles, etc.

Poner las cosas juntas en una jerarquía no es realmente especial. ¡Es solo otro colector que estoy seguro que usarás con mucha más frecuencia a partir de ahora!

Si quieres conocer otros artículos parecidos a Cómo crear jerarquías de objetos Java a partir de listas planas con Collector puedes visitar la categoría Código.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Esta página web utiliza cookies para analizar de forma anónima y estadística el uso que haces de la web, mejorar los contenidos y tu experiencia de navegación. Para más información accede a la Política de Cookies . Ver mas