Introducción

A veces una aplicación necesita operar sobre datos jerárquicos que se encuentran en una BBDD, es decir, tablas que tienen una relación sobre sí mismas (del tipo categoría-subcategorías). Cuando las operaciones son sencillas no se necesita nada especial, pero en cuanto es necesario filtrar datos por niveles de la jerarquía u obtener un nodo concreto del árbol, la cosa se complica, ya que los mecanismos que ponen a nuestra disposición tanto Java como la mayoría de frameworks, son limitados en este aspecto.

Los tipos de solución que detallo más adelante, están basados en un árbol de categorías, una forma simple de entender e imaginar una jerarquía de datos.

 

Ejemplo de categorías

Tabla Category

id

parent_id

description

1

null

Item1

2

1

Item1.1

3

1

Item1.2

4

null

Item2

5

4

Item2.1

6

5

Item2.1.1

7

5

Item2.1.2

8

7

Item2.1.2.1

9

7

Item2.1.2.2

10

7

Item2.1.2.3

 

Si pensamos en el árbol que corresponde con la jerarquía de datos de la tabla de categorías, se traduciría en esto:

  • Item1

    • Item1.1

    • Item1.2

  • Item2

    • Item2.1

      • Item2.1.1

      • Item2.1.2

        • Item2.1.2.1

        • Item2.1.2.2

        • Item2.1.2.3

 

 

Tipos de solución

Hay varias formas de afrontar el problema, que dependerán del framework y arquitectura del proyecto, el tamaño de los datos, el peso que se otorgue a la optimización, etc.

 

Query nativa

La opción menos deseable, pero probablemente la más rápida y fácil de implementar.

Esta opción trae consigo varias desventajas, que la hacen por lo general la peor opción. Entre sus principales desventajas se encuentran la difícil legibilidad de escribir queries en el código y la dependencia con el motor de BBDD.

 

Ejemplo con query nativa de Oracle SQL

SELECT
    id,
    parent_id,
    level,
    description,
FROM
    category
WHERE
    level <= 3
START WITH
    id = 4 /* Nodo inicial del árbol */
CONNECT BY
    PRIOR id = parent_id;

 

Se pueden observar las palabras clave “level”, “start with” y “connect by prior” que permiten acceder a la estructura de árbol de una tabla con datos jerárquicos.

Esta query mostraría los siguientes resultados:

id

parent_id

level

description

4

null

1

Item2

5

4

2

Item2.1

6

5

3

Item2.1.1

7

5

3

Item2.1.2

 

 

Vista de BBDD

Esta opción generalmente es mejor que una query nativa —probablemente la mejor opción—, ya que de cara a la aplicación, nos comunicamos con una vista (que es equivalente a una tabla). La contrapartida, es que requiere el mantenimiento de un nuevo elemento en la BBDD, la nueva vista.

Crear una vista no tiene misterio, siguiendo el ejemplo anterior, podríamos crearla utilizando la query nativa. La ventaja es que para la aplicación sería transparente cómo esté creada la vista, la trataríamos como a una tabla de consulta.

La clave está en que la vista contenga el campo «level», para poder hacer consultas simples desde Java. Por ejemplo, esto nos permitiría traernos todo el árbol y hacer la consulta con Java Stream API, crear una query JPQL, crear una query con Criteria API, usar los Repository de Spring Framework, etc.

 

JPQL/Creiteria API

En la teoría esta sería la opción más deseable, ya que es la más flexible y agnóstica al motor de BBDD, sin embargo existen una serie de limitaciones a la hora de realizar consultas sobre tablas jerárquicas que impiden realizar consultas óptimas.

El concepto de nivel y nodo no están disponibles en JQPL o Creiteria API y eso provoca que tengamos que traernos casi todo el bloque de datos y realizar un filtrado manual en Java.

Si el volumen de datos es pequeño y el rendimiento no es un problema, esta sería una opción a tener en cuenta.

 

EclipseLink + JPQL/Criteria API

Con EclipseLink podemos crear consultas jerárquicas limitadas, es una opción intermedia que nos puede ayudar a reducir el volumen de datos inicial sobre los que operar. La principal desventaja es que dependemos de EclipseLink.

 

Entidad Category

@Entity
public class Category {
    @Id
    private Long id;
    
    private String description;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PARENT_ID", insertable = false, updatable = false)
    private Category parent;

    @OneToMany(mappedBy = "parent")
    private Set<Category> children = new HashSet<>();

    @Transient
    public int getTreeLevel() {
        int level = 1;
        Category currentNode = this;

        while (!currentNode.isRoot()) {
            level++;
            currentNode = currentNode.getParent();
        }

        return level;
    }

    @Transient
    public boolean isRoot() {
        return getParent() == null;
    }
}

 

Método de consulta

@SuppressWarnings("unchecked")
public List<Category> findOnTree(Long fromNodeId, int maxLevel, List<Long> blackList)
        throws IllegalStateException, SecurityException, SystemException {

    ExpressionBuilder builder = new ExpressionBuilder(Category.class);
    Expression startWith = builder.get("id").equal(fromNodeId);
    Expression connectBy = builder.get("children");

    ReadAllQuery query = new ReadAllQuery(Category.class);
    query.setSelectionCriteria(
            builder.get("id").notIn(blackList).and(builder.get("parent").notIn(blackList)));
    query.setHierarchicalQueryClause(startWith, connectBy, null);

    List<Category> result = (List<GgrlSapVOrganics>) EntityManagerHelper
            .entityManager()
            .unwrap(UnitOfWork.class)
            .executeQuery(query);

    return result.stream().filter(o -> o.getTreeLevel() <= maxLevel).collect(Collectors.toList());
}

 

Hay que tener en cuenta que esta opción no permite filtrar por “nivel” en BBDD, por lo que el bloque de datos traerá todos los sub-nodos del nodo indicado con el “startWith”. A cambio, nos permite filtrar a partir de un nodo concreto, y ordena el resultado teniendo en cuenta la jerarquía.

La clave está en la ReadAllQuery.setHierarchicalQueryClause(…), que proporciona EclipseLink y que se asemeja al concepto utilizado en al query nativa.

 

Conclusión

A la hora de decidir qué opción utilizamos en nuestros proyectos, tendremos que tener en cuenta la arquitectura que se utiliza, las restricciones del cliente y el tiempo del que disponemos para desarrollar.

En general diría que la mejor opción para estos casos es utilizar una vista de BBDD, siempre que esta vista sea suficientemente genérica y reutilizable, pero siempre es importante el contexto del proyecto, y es este quien nos marcará cuál es la opción que debemos utilizar y la que mejor se adapta a la situación.

Os animo a dejar comentarios sobre otras soluciones, frameworks o librerías que conozcáis para filtrar objetos jerarquícos.

Foto de capçalera de Basetis
Árbol,java,Jerarquías,Open,Query,soluciones,