Una guía para SqlResultSetMapping

1. Introducción

En esta guía, echaremos un vistazo a SqlResultSetMapping , de la API de persistencia de Java (JPA).

La funcionalidad principal aquí implica mapear conjuntos de resultados de declaraciones SQL de base de datos en objetos Java.

2. Configuración

Antes de ver su uso, hagamos algunas configuraciones.

2.1. Dependencia de Maven

Nuestras dependencias Maven requeridas son Hibernate y H2 Database. Hibernate nos da la implementación de la especificación JPA. Usamos H2 Database para una base de datos en memoria.

2.2. Base de datos

A continuación, crearemos dos tablas como se ve aquí:

CREATE TABLE EMPLOYEE (id BIGINT, name VARCHAR(10));

La tabla EMPLOYEE almacena un objeto Entity de resultado . SCHEDULE_DAYS contiene registros vinculados a la tabla EMPLOYEE por la columna employeeId:

CREATE TABLE SCHEDULE_DAYS (id IDENTITY, employeeId BIGINT, dayOfWeek VARCHAR(10));

Se puede encontrar un script para la creación de datos en el código de esta guía.

2.3. Objetos de entidad

Nuestros objetos Entity deberían verse similares:

@Entity public class Employee { @Id private Long id; private String name; }

Los objetos de entidad pueden tener un nombre diferente al de las tablas de la base de datos. Podemos anotar la clase con @ Table para mapearlos explícitamente:

@Entity @Table(name = "SCHEDULE_DAYS") public class ScheduledDay { @Id @GeneratedValue private Long id; private Long employeeId; private String dayOfWeek; }

3. Mapeo escalar

Ahora que tenemos datos, podemos comenzar a mapear los resultados de la consulta.

3.1. ColumnResult

Si bien las anotaciones SqlResultSetMapping y Query también funcionan en las clases de Repository , usamos las anotaciones en una clase Entity en este ejemplo.

Cada anotación SqlResultSetMapping requiere solo una propiedad, el nombre. Sin embargo, sin uno de los tipos de miembros, no se asignará nada. Los tipos de miembros son ColumnResult , ConstructorResult y EntityResult .

En este caso, ColumnResult asigna cualquier columna a un tipo de resultado escalar:

@SqlResultSetMapping( name="FridayEmployeeResult", columns={@ColumnResult(name="employeeId")})

El nombre de la propiedad ColumnResult identifica la columna en nuestra consulta:

@NamedNativeQuery( name = "FridayEmployees", query = "SELECT employeeId FROM schedule_days WHERE dayOfWeek = 'FRIDAY'", resultSetMapping = "FridayEmployeeResult") 

Tenga en cuenta que el valor de resultSetMapping en nuestra anotación NamedNativeQuery es importante porque coincide con la propiedad de nombre de nuestra declaración ResultSetMapping .

Como resultado, el conjunto de resultados de NamedNativeQuery se asigna como se esperaba. Asimismo, StoredProcedure API requiere esta asociación.

3.2. Prueba ColumnResult

Necesitaremos algunos objetos específicos de Hibernate para ejecutar nuestro código:

@BeforeAll public static void setup() { emFactory = Persistence.createEntityManagerFactory("java-jpa-scheduled-day"); em = emFactory.createEntityManager(); }

Finalmente, llamamos a la consulta nombrada para ejecutar nuestra prueba:

@Test public void whenNamedQuery_thenColumnResult() { List employeeIds = em.createNamedQuery("FridayEmployees").getResultList(); assertEquals(2, employeeIds.size()); }

4. Mapeo de constructores

Echemos un vistazo a cuándo necesitamos mapear un conjunto de resultados a un objeto completo.

4.1. ConstructorResult

De manera similar a nuestro ejemplo de ColumnResult , agregaremos la anotación SqlResultMapping en nuestra clase Entity , ScheduledDay . Sin embargo, para mapear usando un constructor, necesitamos crear uno:

public ScheduledDay ( Long id, Long employeeId, Integer hourIn, Integer hourOut, String dayofWeek) { this.id = id; this.employeeId = employeeId; this.dayOfWeek = dayofWeek; }

Además, el mapeo especifica la clase y las columnas de destino (ambas requeridas):

@SqlResultSetMapping( name="ScheduleResult", classes={ @ConstructorResult( targetClass=com.baeldung.sqlresultsetmapping.ScheduledDay.class, columns={ @ColumnResult(name="id", type=Long.class), @ColumnResult(name="employeeId", type=Long.class), @ColumnResult(name="dayOfWeek")})})

El orden de ColumnResults es muy importante. Si las columnas están desordenadas, el constructor no podrá ser identificado. En nuestro ejemplo, el orden coincide con las columnas de la tabla, por lo que en realidad no sería necesario.

@NamedNativeQuery(name = "Schedules", query = "SELECT * FROM schedule_days WHERE employeeId = 8", resultSetMapping = "ScheduleResult")

Otra diferencia única para ConstructorResult es que la instanciación del objeto resultante es "nuevo" o "separado". La entidad mapeada estará en el estado separado cuando exista una clave primaria coincidente en el EntityManager; de lo contrario, será nueva.

A veces, podemos encontrar errores en tiempo de ejecución debido a que los tipos de datos SQL no coinciden con los tipos de datos Java. Por lo tanto, podemos declararlo explícitamente con type.

4.2. Prueba ConstructorResult

Probemos el ConstructorResult en una prueba unitaria:

@Test public void whenNamedQuery_thenConstructorResult() { List scheduleDays = Collections.checkedList( em.createNamedQuery("Schedules", ScheduledDay.class).getResultList(), ScheduledDay.class); assertEquals(3, scheduleDays.size()); assertTrue(scheduleDays.stream().allMatch(c -> c.getEmployeeId().longValue() == 3)); }

5. Mapeo de entidades

Finalmente, para un mapeo de entidad simple con menos código, echemos un vistazo a EntityResult .

5.1. Entidad única

EntityResult requires us to specify the entity class, Employee. We use the optional fields property for more control. Combined with FieldResult, we can map aliases and fields that do not match:

@SqlResultSetMapping( name="EmployeeResult", entities={ @EntityResult( entityClass = com.baeldung.sqlresultsetmapping.Employee.class, fields={ @FieldResult(name="id",column="employeeNumber"), @FieldResult(name="name", column="name")})})

Now our query should include the aliased column:

@NamedNativeQuery( name="Employees", query="SELECT id as employeeNumber, name FROM EMPLOYEE", resultSetMapping = "EmployeeResult")

Similarly to ConstructorResult, EntityResult requires a constructor. However, a default one works here.

5.2. Multiple Entities

Mapping multiple entities is pretty straightforward once we have mapped a single Entity:

@SqlResultSetMapping( name = "EmployeeScheduleResults", entities = { @EntityResult(entityClass = com.baeldung.sqlresultsetmapping.Employee.class), @EntityResult(entityClass = com.baeldung.sqlresultsetmapping.ScheduledDay.class)

5.3. EntityResult Tests

Let's have a look at EntityResult in action:

@Test public void whenNamedQuery_thenSingleEntityResult() { List employees = Collections.checkedList( em.createNamedQuery("Employees").getResultList(), Employee.class); assertEquals(3, employees.size()); assertTrue(employees.stream().allMatch(c -> c.getClass() == Employee.class)); }

Since the multiple entity results join two entities, the query annotation on only one of the classes is confusing.

Por eso, definimos la consulta en la prueba:

@Test public void whenNamedQuery_thenMultipleEntityResult() { Query query = em.createNativeQuery( "SELECT e.id, e.name, d.id, d.employeeId, d.dayOfWeek " + " FROM employee e, schedule_days d " + " WHERE e.id = d.employeeId", "EmployeeScheduleResults"); List results = query.getResultList(); assertEquals(4, results.size()); assertTrue(results.get(0).length == 2); Employee emp = (Employee) results.get(1)[0]; ScheduledDay day = (ScheduledDay) results.get(1)[1]; assertTrue(day.getEmployeeId() == emp.getId()); }

6. Conclusión

En esta guía, analizamos diferentes opciones para usar la anotación SqlResultSetMapping . SqlResultSetMapping es una parte clave de la API de persistencia de Java.

Los fragmentos de código se pueden encontrar en GitHub.