Comprendiendo los Tipos de Joins en JPA con Java

Introducción

En este artículo, exploraremos los diferentes tipos de joins soportados por JPA (Java Persistence API) y cómo podemos utilizarlos eficazmente en nuestros proyectos de programación en JAVA. Esto es de suma importancia para todos los programadores que trabajan con bases de datos en sus aplicaciones JAVA, ya que un manejo adecuado de los joins puede optimizar las consultas y mejorar la eficiencia del acceso a los datos.

A medida que profundicemos en el contenido, usaremos JPQL (Java Persistence Query Language) como nuestro lenguaje de consultas, lo que nos permitirá realizar operaciones de joins de manera sencilla y eficiente.

1. Overview

En este tutorial, analizaremos los diferentes tipos de joins soportados por JPA. Como mencionamos, utilizaremos JPQL, un lenguaje de consulta para JPA.

2. Sample Data Model

Para ilustrar las consultas, crearemos un modelo de datos de ejemplo que utilizaremos en las operaciones de joins.

Empezaremos con la entidad Employee (Empleado):


@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String name;

    private int age;

    @ManyToOne
    private Department department;

    @OneToMany(mappedBy = "employee")
    private List phones;

    // getters y setters...
}
    

Cada Employee estará asignado a un único Department (Departamento):


@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private String name;

    @OneToMany(mappedBy = "department")
    private List employees;

    // getters y setters...
}
    

Finalmente, cada Employee tendrá múltiples Phone (teléfonos):


@Entity
public class Phone {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String number;

    @ManyToOne
    private Employee employee;

    // getters y setters...
}
    

3. Inner Joins

Comenzaremos con los inner joins. Cuando se combinan dos o más entidades mediante un inner join, solo se recogen los registros que coinciden con la condición de join en el resultado.

3.1. Implicit Inner Join With Single-Valued Association Navigation

Los inner joins pueden ser implícitos. Como su nombre indica, el desarrollador no especifica explícitamente los inner joins. Cada vez que navegamos a través de una asociación de valor único, JPA crea automáticamente un join implícito:


@Test
public void whenPathExpressionIsUsedForSingleValuedAssociation_thenCreatesImplicitInnerJoin() {
    TypedQuery query
      = entityManager.createQuery(
          "SELECT e.department FROM Employee e", Department.class);
    List resultList = query.getResultList();
    
    // Assertions...
}
    

Aquí, la entidad Employee tiene una relación de muchos a uno con la entidad Department. Cuando navegamos desde un Employee hacia su Department especificando e.department, JPA crea un inner join automáticamente y la condición del join se deriva de la metadata de mapeo.

3.2. Explicit Inner Join With Single-Valued Association

Ahora veremos los inner joins explícitos, donde utilizamos la palabra clave JOIN en nuestra consulta JPQL:


@Test
public void whenJoinKeywordIsUsed_thenCreatesExplicitInnerJoin() {
    TypedQuery query
      = entityManager.createQuery(
          "SELECT d FROM Employee e JOIN e.department d", Department.class);
    List resultList = query.getResultList();
    
    // Assertions...
}
    

En esta consulta, especificamos la palabra clave JOIN y la entidad Department en la cláusula FROM. A pesar de esta diferencia sintáctica, las consultas SQL resultantes serán muy similares.

También podemos especificar opcionalmente la palabra clave INNER:


@Test
public void whenInnerJoinKeywordIsUsed_thenCreatesExplicitInnerJoin() {
    TypedQuery query
      = entityManager.createQuery(
          "SELECT d FROM Employee e INNER JOIN e.department d", Department.class);
    List resultList = query.getResultList();

    // Assertions...
}
    

3.3. Explicit Inner Join With Collection-Valued Associations

Otro caso en el que debemos ser explícitos es con las asociaciones valoradas como colecciones. Si analizamos nuestro modelo de datos, el Employee tiene una relación de uno a muchos con Phone. Como en el ejemplo anterior, intentemos escribir una consulta similar:


SELECT e.phones FROM Employee e
    

Sin embargo, esto no funcionará como podríamos haber esperado. Dado que la asociación seleccionada, e.phones, es valorada como colección, obtendremos una lista de Collections en lugar de entidades Phone:


@Test
public void whenCollectionValuedAssociationIsSpecifiedInSelect_ThenReturnsCollections() {
    TypedQuery query 
      = entityManager.createQuery(
          "SELECT e.phones FROM Employee e", Collection.class);
    List resultList = query.getResultList();

    // Assertions...
}
    

Si deseamos filtrar entidades Phone en la cláusula WHERE, JPA no lo permitirá, ya que una expresión de ruta no puede continuar desde una asociación valorada como colección. Por ejemplo, e.phones.number no es válido.

En su lugar, deberíamos crear un inner join explícito y asignar un alias a la entidad Phone. Luego, podremos especificar la entidad Phone en la cláusula SELECT o WHERE:


@Test
public void whenCollectionValuedAssociationIsJoined_ThenCanSelect() {
    TypedQuery query 
      = entityManager.createQuery(
          "SELECT ph FROM Employee e JOIN e.phones ph WHERE ph.number LIKE '1%'", Phone.class);
    List resultList = query.getResultList();
    
    // Assertions...
}
    

4. Outer Join

Cuando se combinan dos o más entidades mediante un outer join, se recogen los registros que satisfacen la condición de join, así como los registros de la entidad de la izquierda:


@Test
public void whenLeftKeywordIsSpecified_thenCreatesOuterJoinAndIncludesNonMatched() {
    TypedQuery query 
      = entityManager.createQuery(
          "SELECT DISTINCT d FROM Department d LEFT JOIN d.employees e", Department.class);
    List resultList = query.getResultList();

    // Assertions...
}
    

En este caso, el resultado contendrá Departments que tienen empleados asociados y también aquellos que no tienen ninguno. Este tipo de join se conoce como un left outer join. JPA no proporciona joins derechas, pero podemos simular joins derechas intercambiando las entidades en la cláusula FROM.

5. Joins in the WHERE Clause

5.1. With a Condition

Podemos listar dos entidades en la cláusula FROM y luego especificar la condición de join en la cláusula WHERE. Esto puede ser útil, especialmente cuando las claves foráneas a nivel de base de datos no están presentes:


@Test
public void whenEntitiesAreListedInFromAndMatchedInWhere_ThenCreatesJoin() {
    TypedQuery query 
      = entityManager.createQuery(
          "SELECT d FROM Employee e, Department d WHERE e.department = d", Department.class);
    List resultList = query.getResultList();
    
    // Assertions...
}
    

Aquí estamos uniendo las entidades Employee y Department, pero esta vez especificando una condición en la cláusula WHERE.

5.2. Without a Condition (Cartesian Product)

De manera similar, podemos listar dos entidades en la cláusula FROM sin especificar ninguna condición de join. En este caso, obtendremos un producto cartesiano de resultados, lo que significa que cada registro en la primera entidad se empareja con cada registro en la segunda entidad:


@Test
public void whenEntitiesAreListedInFrom_ThenCreatesCartesianProduct() {
    TypedQuery query
      = entityManager.createQuery(
          "SELECT d FROM Employee e, Department d", Department.class);
    List resultList = query.getResultList();
    
    // Assertions...
}
    

Este tipo de consulta, aunque es válida, no será eficiente.

6. Multiple Joins

Hasta ahora, hemos utilizado dos entidades para realizar joins, pero esto no es una regla. Podemos unir múltiples entidades en una sola consulta JPQL:


@Test
public void whenMultipleEntitiesAreListedWithJoin_ThenCreatesMultipleJoins() {
    TypedQuery query 
      = entityManager.createQuery(
          "SELECT ph FROM Employee e JOIN e.department d JOIN e.phones ph WHERE d.name IS NOT NULL", Phone.class);
    List resultList = query.getResultList();
    
    // Assertions...
}
    

Aquí estamos seleccionando todos los Phones de todos los Employees que tienen un Department. Al igual que otros inner joins, no estamos especificando condiciones mientras que JPA extrae esta información de la metadata de mapeo.

7. Fetch Joins

Ahora hablemos sobre los fetch joins. Su uso principal es para cargar asociaciones que se cargan de manera perezosa de forma ansiosa para la consulta actual:


@Test
public void whenFetchKeywordIsSpecified_ThenCreatesFetchJoin() {
    TypedQuery query 
      = entityManager.createQuery(
          "SELECT d FROM Department d JOIN FETCH d.employees", Department.class);
    List resultList = query.getResultList();
    
    // Assertions...
}
    

Si bien esta consulta se parece a otras consultas, hay una diferencia: los Employees se cargan de manera ansiosa. Esto significa que, una vez que llamemos a getResultList en la prueba anterior, las entidades de Department tendrán su campo employees cargado, ahorrándonos otro viaje a la base de datos.

Sin embargo, debemos ser conscientes del intercambio de memoria. Podemos ser más eficientes porque solo realizamos una consulta, pero también hemos cargado en memoria todas las Departments y sus empleados de una vez.

También podemos realizar un outer fetch join de manera similar a los outer joins, donde recogemos registros de la entidad de la izquierda que no coinciden con la condición de join. Además, carga ansiosamente la asociación especificada:


@Test
public void whenLeftAndFetchKeywordsAreSpecified_ThenCreatesOuterFetchJoin() {
    TypedQuery query 
      = entityManager.createQuery(
          "SELECT d FROM Department d LEFT JOIN FETCH d.employees", Department.class);
    List resultList = query.getResultList();
    
    // Assertions...
}
    

8. Summary

En este artículo, cubrimos los tipos de joins en JPA, explorando tanto los joins implícitos como explícitos y cómo afectan la recuperación de datos en aplicaciones JAVA. Al entender los métodos de join de JPA, podrás optimizar tus consultas y obtener un mejor rendimiento de tus aplicaciones.

Consejos Prácticos para Programadores de JAVA:

  • Siempre que sea posible, utiliza joins explícitos para mayor claridad en tu código.
  • Recuerda que los joins implícitos podrían ser más difíciles de leer y comprender en consultas complejas.
  • Cuida el uso de joins múltiples, ya que pueden afectar el rendimiento de tu aplicación si no se manejan con cuidado.
  • Aprovecha los fetch joins para materia de frecuencia de acceso a los datos, pero ten en cuenta el uso de la memoria.
  • Realiza pruebas y análisis de rendimiento en tus consultas para asegurarte de que están optimizadas.

Con todo esto en mente, ¡buena suerte en la implementación de joins en tus aplicaciones JAVA!