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.
Comentaris