Curso Jakarta EE 9 (34). JPA con Hibernate (17): el lenguaje JPQL\HQL (2). Reuniones.

logo Jakarta EE

Hasta ahora, nos hemos conformado con usar una única entidad en el FROM de las consultas JPQL. Para combinar datos de varias necesitamos hacer una reunión o join entre ellas, tal y como haríamos con tablas en una consulta SQL. En este capítulo examinamos los distintos tipos de reuniones disponibles.

>>>> ÍNDICE <<<<

Reuniones internas

El tipo de reunión de entidades más empleado es la denominada reunión interna (INNER JOIN). Esta operación obtiene todos los datos que verifiquen el «punto de unión» de las entidades dado por la configuración de una relación. Es decir, obtenemos los dos extremos de una relación si ambos existen. Luego en el WHERE haremos una selección de esos datos cuando sea necesario.

Recuperemos los gastos de una cuantía igual o superior a la indicada por cierto parámetro (minAmount). Además del gasto, queremos conocer el nombre de su categoría, lo que nos obliga a declarar una variable para la entidad Category que podamos usar en la proyección. Pero no sirve cualquier categoría, tiene que ser la asociada a cada gasto.

SELECT e, c.name 
FROM Expense e INNER JOIN e.category c
WHERE e.amount >= :minAmount

Aquí se ha reunido cada gasto con su categoría con el fin de poder seleccionar aquellos «emparejamientos» que verifiquen el where. Se puede usar INNER JOIN o JOIN, pues INNER es el modo predeterminado. En la declaración del JOIN proporcionaremos la relación que debe usarse.

Esta es la consulta SQL equivalente. Las condiciones para la reunión se indican con ON, y queremos emparejar gastos y categorías asociando la clave ajena de expense con la clave primaria de su categoría.

SELECT e.*, c.name
FROM  expenses e JOIN categories e ON c.id = e.category_id
WHERE e.amount >= :minAmount

Al igual que en SQL, en JPQL se pueden reunir entidades declarando el punto de unión en el WHERE. Esta es una versión alternativa de la misma consulta.

SELECT e, c.name
FROM Expense c, Category c
WHERE e.category.id = c.id AND e.amount >= :minAmout

En realidad, estamos haciendo un CROSS JOIN o, en términos matemáticos, el producto cartesiano de las entidades: se realizan todas las combinaciones posibles de todos los datos de las entidades y en el WHERE seleccionamos aquellas que verifican ciertos criterios. Si alguno vincula las entidades por la clave ajena, la base de datos se percatará de ello y actuará como si de un INNER JOIN se tratase y con el mismo rendimiento.

No obstante, la sintaxis anterior resulta menos legible que la declaración explícita de INNER JOIN y no puede aplicarse a las reuniones externas (OUTER JOIN) que veremos en breve. Sin embargo, es útil debido a que JPQL solo permite reunir entidades relacionadas. Esta limitación es más importante de lo que parece a simple vista, ya que en ocasiones condicionará el diseño del modelo de entidades. Por fortuna, HQL no cuenta con esta restricción y es posible reunir entidades aunque no estén relacionadas con la misma sintaxis de SQL.

SELECT e, c.name
FROM Expense e JOIN Category c ON e.category.id = c.id 
WHERE e.amount >= :minAmount

Lo que nunca podemos hacer, ni siquiera con HQL, es incluir subconsultas dentro del bloque FROM. En consecuencia, recurriremos a SQL si necesitamos trabajar con tablas derivadas.

A veces, tendremos reuniones implícitas que podríamos calificar como «ocultas»: no se declaran de forma explícita en JPQL, pero aparecerán en el SQL generado por Hibernate. Esto se produce cuando «navegamos» por las relaciones con los nombres de los atributos separados por el operador punto (.), una de las capacidades más llamativas de JPQL. Es el caso de este ejemplo.

SELECT e, e.category.name 
FROM Expense e
WHERE e.amount >= :minAmount

Con e.category.name se accede en primer lugar a la categoría del gasto, y luego a su nombre. Lo cierto es que es la misma consulta que he usado como ejemplo de INNER JOIN. Cambia la sintaxis, no la manera en la que se obtienen los datos: en SQL hay que unir sin remedio las tablas. Hibernate lo deduce y genera esta sentencia.

SELECT
	expense0_.id as col_0_0_,
	category1_.name as col_1_0_,
	expense0_.id as id1_4_,
	expense0_.amount as amount2_4_,
	expense0_.category_id as category6_4_,
	expense0_.comments as comments3_4_,
	expense0_.concept as concept4_4_,
	expense0_.date as date5_4_
FROM
	expenses expense0_ CROSS JOIN categories category1_
WHERE
	expense0_.category_id = category1_.id
	and expense0_.amount >= 1

En las relaciones múltiples, modeladas con una colección en vez de una entidad, no es posible acceder a los atributos o relaciones de cada elemento. Esto es incorrecto.

SELECT c 
FROM Category c 
WHERE c.expenses.amount > :minAmount

La ejecución provoca una excepción.

org.hibernate.QueryException: illegal attempt to dereference collection [category0_.id.expenses] with element property reference [amount]

Debemos hacer la reunión y declarar una variable que represente a cada gasto del listado.

SELECT c 
FROM Category c JOIN c.expenses e 
WHERE e.amount > 1

La posibilidad de navegar a través de las relaciones ayuda a escribir consultas simples y concisas, aunque perdemos algo de control sobre el resultado final que en algunos casos puede perjudicar el rendimiento. Veámoslo con otro ejemplo, basado en la relación many-to-many que asocia los presupuestos con las categorías. Obtiene todas las categorías que comprende el presupuesto con identificador id.

SELECT b.categories 
FROM Budget b 
WHERE b.id = :id

Aunque alcanzamos nuestro objetivo, en la bitácora veremos que se han reunido tres tablas.

SELECT
	category2_.id as id1_2_,
	category2_.color_hex as color_he2_2_,
	category2_.name as name3_2_
FROM
	budgets budget0_
INNER JOIN budgets_categories categories1_ on
	budget0_.id = categories1_.budget_id
INNER JOIN categories category2_ on
	categories1_.category_id = category2_.id
WHERE
	budget0_.id = :id

El lector perspicaz notará que no hace falta unir budgets y budgets_category porque con esta última ya podemos filtrar por el identificador del presupuesto. Así pues, esta consulta es más simple y eficiente.

SELECT c.*
FROM categories c JOIN budgets_categories bc ON bc.category_id = c.id
WHERE bc.budget_id = :id

En este caso concreto, la triple unión se genera en JPQL (y Criteria API) porque en la consulta hay que hacer referencia a las entidades Budget y Category (la tabla intermedia no tiene una entidad) para seleccionar los datos. En la mayoría de casos no debería causar un problema de rendimiento. Las alternativas son escribir la consulta en SQL o descomponer la relación en one-to-many y many-to-one con una clase asociación, justo como expliqué en el capítulo 25.

La navegación por las relaciones también es válida en las cláusulas WHERE y ORDER BY. Aquí la existencia de reuniones implícitas suele ser menos evidente.

SELECT c
FROM Coupon c 
WHERE c.expense.category.id = :id

Estamos obteniendo los cupones empleados en el pago de gastos de cierta categoría, lo que requiere acceder a las tablas de gastos y cupones. Dado que la primera contiene la clave ajena con la categoría, no es necesaria la reunión con la tabla categories (no queremos ningún registro de ella).

SELECT
	coupon0_.id as id1_8_,
	coupon0_.amount as amount2_8_,
	coupon0_.expense_id as expense_5_8_,
	coupon0_.expiration as expirati3_8_,
	coupon0_.name as name4_8_
FROM
	coupons coupon0_ cross join expenses expense1_
WHERE
	coupon0_.expense_id = expense1_.id
	and expense1_.category_id = :id

Como hemos visto, las reuniones implícitas se han traducido en reuniones internas (siendo rigurosos, productos cartesianos filtrados en el WHERE). Suele servir, pero cuidado, porque cuando necesitamos las reuniones externas que veremos a continuación tendremos que declararlas explícitamente. Me ha sucedido en varias ocasiones que la inclusión una condición a un WHERE u ORDER BY ha cambiado los resultados devueltos por la consulta porque ha aparecido un INNER JOIN que debería ser un OUTER JOIN.

Reuniones externas – outer join

Las reuniones internas y el producto cartesiano son las formas más habituales de combinar tablas. En JPQL también disponemos de las reuniones externas (OUTER JOIN) de SQL, no menos importantes. En esta modalidad no es necesario que los dos extremos de la relación existan: siempre obtenemos uno de ellos, y la entidad relacionada en el otro lado solo en el caso de que exista. Veamos cómo funciona comparándola con la reunión interna.

En nuestro modelo de entidades, cuando un cupón se utiliza en el pago de un gasto queda asociado a ese gasto. La relación opcional one-to-one entre Expense y Coupon no existe para aquellos cupones que todavía no se han usado. Por tanto, la reunión interna entre ambas entidades únicamente devuelve los cupones relacionados con gastos. Aquellos que no tengan su gasto, se descartan. Es lo que he reflejado en el este gráfico.

Supongamos que disponemos de estos datos.

Con la siguiente consulta obtenemos el único cupón que tiene su gasto (id 2). No hace falta declarar el JOIN si accedemos al gasto en la proyección con «c.expense.», lo he puesto para que sea vea más claro.

SELECT c.name, e.concept, e.date 
FROM Coupon c JOIN c.expense e

¿Y si necesitamos todos los cupones sí o sí, más el gasto de aquellos que se hayan usado? Tenemos que hacer la reunión externa de Coupon y Expense con LEFT OUTER JOIN (el OUTER se puede omitir).

SELECT c.name,  e.concept, e.date 
FROM Coupon c LEFT JOIN c.expense e

Dicho de manera coloquial, pedimos TODOS los registros de la entidad que aparece a la izquierda del JOIN, emparejados con aquellos de la derecha que están relacionados. Ahora recibimos todos los cupones, y las proyecciones para el gasto serán nulas cuando este no exista para el cupón.

En la práctica, hemos añadido a los resultados entregados por la reunión interna los cupones sin gasto.

Al igual que sucede con las reuniones internas, la sintaxis de SQL está disponible en HQL. Dado que podemos definir las condiciones que queramos en el ON del JOIN, no estamos limitados a unir entidades relacionadas.

SELECT c.name, e.concept, e.date 
FROM Coupon c LEFT JOIN Expense e ON c.expense.id = e.id

Una característica poco conocida es la posibilidad de seleccionar los datos del lado derecho de la reunión usando ON (JPQL) o WITH (HQL). Esta selección se aplica después de haberse obtenidos los resultados fruto de la reunión.

SELECT c.name, e.concept, e.date
FROM Coupon c LEFT JOIN c.expense e ON e.amount > 200

Seguimos obteniendo todos los cupones (lado izquierdo de la unión) aunque ninguno tenga un gasto superior a 200. Pero ahora, en el cupón 2 el gasto es nulo porque su cuantía de 120.43 no verifica la condición.

Atención a esta consulta.

SELECT c.name, e.concept, e.date 
FROM Coupon c LEFT JOIN c.expense e 
WHERE e.amount > 200

Aunque se parece a la anterior, no hace lo mismo. No devuelve ningún resultado debido a que las condiciones declaradas en el WHERE se aplican al conjunto de datos obtenidos en la reunión, es decir, a los emparejamientos entre izquierda y derecha, y ninguno de ellos incluye un gasto mayor que 200.

HQL implementa RIGHT JOIN y FULL JOIN, dos modalidades de reuniones externas que suelen estar disponibles en SQL y de uso poco común. El primero funciona igual que LEFT JOIN, pero realizando la reunión en el sentido contrario: obtiene todas las entidades de la derecha y las empareja con las de la izquierda cuando la relación entre ambos exista. Las siguientes consultas son equivalentes. Téngase en cuenta que Expense no tiene una relación con Coupon, de ahí el empleo de ON para vincular ambas entidades.

FULL JOIN o FULL OUTER JOIN, no disponible en MySQL, recopila las entidades de ambos extremos, estén o no relacionadas, así que tengamos cuidado porque el resultado puede ser gigantesco. Cuando la asociación exista, obtendremos el emparejamiento. Es lo que refleja la versión final del diagrama con las reuniones de cupones y gastos.

Fetch join

Nos encontramos ante un tipo de unión exclusivo de JPQL, no permitido en el FROM de subconsultas. No lo tenemos en SQL, pues su existencia es consecuencia del mecanismo de carga perezosa o LAZY de relaciones.

Atención al siguiente fragmento de código que intenta obtener los cupones aplicados a gastos de la categoría uno, de tal modo que los cupones contengan la entidad de su gasto.

List<Coupon> coupons = em.createQuery("SELECT c FROM Coupon c WHERE c.expense.category.id = 1", 
                                     Coupon.class)
                         .getResultList();
coupons.forEach(c -> c.getExpense().getAmount());

Aunque en SQL se reúnen los cupones y sus gastos para aplicar la condición de selección del WHERE, la consulta devuelve las entidades de Coupon respetando la configuración de sus relaciones. Esto significa que, puesto que la relación entre el cupón y su gasto es de tipo perezoso, los cupones no incluyen su gasto (*) y este se obtendrá bajo demanda. Es lo que se ha hecho en la línea cuatro, lo que causa la ejecución de la SELECT correspondiente.

(*) No olvidemos que si el gasto existe, no es nulo sino un proxy que contiene el identificador, por eso he invocado a un getter. Otra alternativa es emplear el método Hibernate#initialize. Lo explico aquí.

El código anterior es el mismo que usé para demostrar los peligros de la carga perezosa. Vimos que provoca el famoso «problema N + 1» porque se genera para cada cupón una consulta que obtiene el gasto.

Cuanto mayor sea el listado, más consultas y, por ende, peor desempeño de nuestro código. También descubrimos que ocurría lo mismo si declarábamos la relación de tipo inmediato (EAGER).

Lo más eficiente es obtener exclusivamente los datos que necesitemos, minimizando el número de consultas SQL que Hibernate tiene que ejecutar. Y aquí es donde entra en juego la reunión de tipo FETCH JOIN: permite especificar una relación que debe ser incluida en la proyección, sin importar que se haya configurado de tipo LAZY.

SELECT c 
FROM Coupon c JOIN FETCH c.expense 
WHERE c.expense.category.id = 1

La traslación de la consulta a SQL revela la inclusión de las columnas de expenses en el resultado, algo que no sucedía antes a pesar de que se realizaba la unión con esa tabla. Hibernate las usará para establecer en los objetos Coupon su gasto. Así, tenemos los datos que queremos con una sola consulta.

SELECT
	coupon0_.id as id1_8_0_,
	expense1_.id as id1_10_1_,
	coupon0_.amount as amount2_8_0_,
	coupon0_.expense_id as expense_5_8_0_,
	coupon0_.expiration as expirati3_8_0_,
	coupon0_.name as name4_8_0_,
	expense1_.amount as amount2_10_1_,
	expense1_.category_id as category6_10_1_,
	expense1_.comments as comments3_10_1_,
	expense1_.concept as concept4_10_1_,
	expense1_.date as date5_10_1_
FROM
	coupons coupon0_
INNER JOIN expenses expense1_ on
	coupon0_.expense_id = expense1_.id
WHERE
	expense1_.category_id = 1

La cláusula FETCH acompaña a la declaración de una reunión. Recordemos que JOIN equivale a INNER JOIN, por lo que nuestro JOIN FETCH ha generado una reunión interna. Por este motivo, solo se devuelven cupones gastados. Si los queremos todos, ya sabemos que debemos recurrir a LEFT JOIN. Acompañándolo de FETCH, los cupones contendrán el gasto cuando exista

SELECT c 
FROM Coupon c LEFT JOIN FETCH c.expense e
WHERE e.category.id = 1
ORDER BY c.name

La invención de FETCH parece una pequeña genialidad, pero hay dos inconvenientes importantes.

Paginación

Consideremos el siguiente método. Obtiene los presupuestos con categorías relacionadas, incluyendo en las entidades la relación perezosa entre ambas. Como somos buenos programadores, aplicamos paginación.

    @Override
    public Page<Budget> fetchWithCategoriesMemoryPagination(int first, int max) {
        List<Budget> budgets = em.createQuery("SELECT b FROM Budget b JOIN FETCH b.categories  " +
                        "ORDER BY b.name", Budget.class)
                .setFirstResult(first)
                .setMaxResults(max)
                .getResultList();
        return new Page<>(budgets, count(), first, max);
    }

Lo relevante está en el SQL generado. Si echamos un vistazo a la proyección, nos percataremos de que los resultados no son los presupuestos, sino los emparejamientos de estos con sus categorías. Si un presupuesto tiene n categorías, aparecerá n veces repetido en los resultados.

 SELECT
	budget0_.id as id1_0_0_,
	category2_.id as id1_2_1_,
	budget0_.amount as amount2_0_0_,
	budget0_.date_from as date_fro3_0_0_,
	budget0_.date_to as date_to4_0_0_,
	budget0_.name as name5_0_0_,
	category2_.color_hex as color_he2_2_1_,
	category2_.name as name3_2_1_,
	categories1_.budget_id as budget_i1_1_0__,
	categories1_.category_id as category2_1_0__

Mejor con un ejemplo. Para los datos del fichero budgets.yml, dado que el presupuesto con identificador 1 posee dos categorías, la consulta anterior devuelve dos veces ese presupuesto. Luego Hibernate procesará los registros para crear una única entidad Budget con sus dos categorías.

Por tanto, si se aplicase paginación en SQL con LIMIT y OFFSET, los resultados serían caóticos, puesto que se paginarían los emparejamientos entre presupuestos y categorías en vez de los presupuestos. Hibernate lo detecta y realiza la paginación a posteriori, lo que implica que primero tiene que obtener todos los presupuestos que hay en la base de datos. Es decir, hace lo que queremos evitar con la paginación.

Esta circunstancia se notifica con el siguiente mensaje.

WARN  [org.hibernate.hql.internal.ast.QueryTranslatorImpl] (default task-2) HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

Hemos topado con un problema bien conocido que cuenta con una solución típica y astuta. En nuestro ejemplo, primero haremos una consulta que averigüe cuáles son los presupuestos que conforman la página a obtener.

  List<Long> budgetPageIds = em.createQuery("SELECT b.id FROM Budget b ORDER BY b.name",
                           Long.class)
                .setFirstResult(first)
                .setMaxResults(max)
                .getResultList();

A continuación, ejecutaremos una segunda consulta que recupere los presupuestos indicados por la primera junto a sus categorías haciendo uso de FETCH. Ya no importa que no se pueda paginar en la base de datos con SQL-de hecho no lo haremos- porque estamos trabajando con los presupuestos de la página.

List<Budget> budgets = em.createQuery("SELECT DISTINCT b FROM Budget b JOIN FETCH b.categories " +
                        "WHERE b.id IN :ids ORDER BY b.name", Budget.class)
                .setParameter("ids", budgetPageIds)
                .getResultList();

Tenemos dos consultas, pero sin duda merece la pena desde el punto de vista del rendimiento, pues conseguimos paginar con suma eficiencia. Este es el código al completo en el proyecto de ejemplo.

    @Override
    public Page<Budget> fetchWithCategoriesTwoQueries(int first, int max) {
        List<Long> budgetPageIds = em.createQuery("SELECT b.id FROM Budget b ORDER BY b.name",
                        Long.class)
                .setFirstResult(first)
                .setMaxResults(max)
                .getResultList();

        List<Budget> budgets = em.createQuery("SELECT DISTINCT b FROM Budget b JOIN FETCH b.categories " +
                        "WHERE b.id IN :ids ORDER BY b.name", Budget.class)
                .setParameter("ids", budgetPageIds)
                .getResultList();

        return new Page<>(budgets, count(), first, max);
    }
MultipleBagFetchException

Si queremos una categoría con sus dos relaciones múltiples perezosas cuando ambas existan…

@ManyToMany(mappedBy = "categories")
private List<Budget> budgets;

@OneToMany(mappedBy = "category")
private List<Expense> expenses;

…escribiremos lo siguiente.

SELECT c 
FROM Category c JOIN FETCH c.budgets JOIN FETCH c.expenses 
WHERE c.id=:id

Vaya por delante que esta consulta no es nada recomendable en vista de que una categoría puede estar asociada a una ingente cantidad de gastos (y ahora ya no seríamos buenos programadores). En cualquier caso, el interés del ejemplo reside en el hecho de que Hibernate no es capaz de ejecutar la consulta. Lo verifica esta prueba.

    @Test
    void testFindByIdWithRelations() {
        assertThatThrownBy(() -> categoryJpqlDAO.findByIdWithRelationsMultipleBags(
Datasets.CATEGORY_ID_ENTERTAINMENT))
                .hasStackTraceContaining("MultipleBagFetchException");
    }

El test comprueba que se lance esta excepción.

java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.danielme.jakartaee.jpa.entities.Category.budgets, com.danielme.jakartaee.jpa.entities.Category.expenses]

El mensaje de MultipleBagFetchException indica que no se puede hacer un fetch para más de un Bag. Resulta confuso porque Hibernate considera como Bag a las listas sin un índice de ordenación de tipo @OrderColumn.

Esta restricción es debida a que las listas admiten elementos duplicados. En la proyección de la consulta anterior los datos de los gastos y presupuestos aparecen repetidos múltiples veces a consecuencia de los emparejamientos entre categorías, gastos y presupuestos. Hibernate no puede discernir si las repeticiones implican duplicidades en la lista de resultados, o bien se deben a las combinaciones de los registros de las tres tablas.

El problema desaparece si utilizamos Set porque ya no hay dudas: un gasto y un presupuesto solo aparecerán una vez en el resultado final. Recordemos que la gran diferencia entre Set y List es la admisión de elementos repetidos, siempre de acuerdo a los métodos equals y hashCode. De nuevo, remito al lector al capítulo 25.

@ManyToMany(mappedBy = "categories")
private Set<Budget> budgets;

@OneToMany(mappedBy = "category")
private Set<Expense> expenses;

Con todo, el verdadero problema sigue oculto, y no es otro que la cantidad de combinaciones efectuadas.

RESULTADOS SQL = n º categorías * nº gastos * n º presupuestos.

Si en lugar de una categoría obtuviésemos 20, cada una con 30 gastos y 5 presupuestos, nos vamos a 3000 resultados pese a que sería solo necesitamos recuperar 55 registros. Si bien no es una carga llamativa para una base de datos, espero que captes la idea. El crecimiento es exponencial.

Entonces, ¿cuál es la solución? Si resulta factible cambiar de List a Set y tenemos la certeza de que el número de entidades vinculadas siempre será muy pequeña (lo cual es mucho suponer), usar Set es una opción razonable. Otra posibilidad, rocambolesca e intrusiva, es emplear @OrderColumn para que alguna de las listas deje de ser un Bag. Pero lo seguro y eficiente es realizar más de una consulta aplicando la misma estrategia que nos permitió combinar FETCH y paginación. Por ejemplo, algo así.

    @Override
    public Optional<Category> findByIdWithRelations(Long id) {
        Optional<Category> category = em.createQuery("SELECT c FROM Category c " +
                "JOIN FETCH c.budgets WHERE c.id=:id", Category.class)
                .setParameter("id", id)
                .getResultStream()
                .findFirst();
        if (category.isEmpty()) {
            return category;
        }
        return em.createQuery("SELECT c FROM Category c " +
                "JOIN FETCH c.expenses WHERE c.id=:id", Category.class)
                .setParameter("id", id)
                .getResultStream()
                .findFirst();
    }

La categoría que devuelve la segunda consulta contiene tanto los presupuestos como los gastos gracias a la magia de la caché de primer nivel (ambas categorías son el mismo objeto).

Semi-join y anti-join

Cierro el capítulo hablando brevemente de dos conceptos relativos a las reuniones, válidos tanto en JPQL como SQL. No son demasiado conocidos, quizás porque no cuentan con operadores específicos para aplicarlos. La siguiente consulta JPQL proviene del capítulo 31 y obtiene las categorías para las que existe al menos un presupuesto.

SELECT c
FROM Category c 
WHERE EXISTS (
    SELECT 1 
    FROM Budget b JOIN b.categories bc 
    WHERE bc.id = c.id )

La estructura anterior se denomina SEMI-JOIN porque comprueba si una hipotética reunión (categorías y presupuestos) devuelve algún resultado. Evita declarar la reunión por no ser necesario obtener los emparejamientos de los registros (solo queremos recuperar las categorías). Si la condición es de no existencia (NOT EXISTS), tendríamos la operación contraria, denominada ANTI-JOIN: queremos saber si una reunión no devuelve resultados sin tener que realizarla. Los mismos conceptos son aplicables si en lugar de EXISTS y NOT EXISTS usamos IN y NOT IN.

Comparémosla con la consulta equivalente basada en una reunión interna.

SELECT c
FROM Category c INNER JOIN b.categories

La versión SEMI-JOIN es más legible porque expresa mejor la intencionalidad de la consulta. Si tienes dudas, basta con que leas el código tal cual y compares. «Obtén las categorías reuniéndolas con sus presupuestos» versus «Obtén las categorías para las que existe al menos un presupuesto». Yo me quedo con la segunda lectura sin dudarlo.

En lo que respecta al rendimiento, parece razonable suponer que la versión con SEMI-JOIN es más eficiente. No obstante, esto dependerá de la estrategia que la base de datos decida seguir para obtener la información. El mero hecho de que con SEMI-JOIN podamos expresar mejor los resultados que queremos es suficiente para justificar su uso preferente.

Código de ejemplo

El código de ejemplo se encuentra en GitHub (todos los proyectos son independientes pero están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.

>>>> ÍNDICE <<<<

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.