JPA-Hibernate + POJO + Projection + ConstructorResult + NativeQuery + Testcontainers + PostgreSQL


En esta ocasión mostraré un ejemplo usando Java SE 11 de como mapear una clase no entidad (un POJO) a partir de los datos que existen en la base de datos y que representan a una entidad utilizando ConstructorResult, Projection y una construcción "manual" con JPA. Las pruebas serán realizadas usando Testcontainers junto a Postgresql como base de datos.

El procedimiento es el siguiente (se omiten los metodos/imports/anotaciones que no son relevantes y usaré lombok solo para getter/setter):

1- La Entidad
Representa la entidad que mantendrá datos en la base de datos y de la cual extraeremos algunos de sus campos para nuestro pojo.
@Entity
@Table(schema = "jpa", name = "persona")
public class Persona {
    @Id
    private int id;
    private String nombre;
    private String apellido;
}


2- El POJO
Requiere tener un constructor con los argumentos que la query utilizará:
public class PersonaPOJO {
    private String nn;
    private String aa;
    
    public PersonaPOJO() {}

    public PersonaPOJO(String nn, String aa) {
        this.nn = nn;
        this.aa = aa;
    }
}
Los atributos pueden tener el nombre que estimen conveniente.


3- Definir el ConstructorResult
@SqlResultSetMapping(name = "personapojo",
  classes = {
    @ConstructorResult(targetClass = PersonaPOJO.class,
      columns = {
        @ColumnResult(name = "nn", type = String.class),
        @ColumnResult(name = "aa", type = String.class)})})

personapojo: es el nombre que utilizaremos para hacer referencia a él
targetClass: nuestra clase objetivo (el POJO)
columns: definicion del resultado de la query a las propiedades que serán mapeadas a nuestro POJO usando el constructor

Para que sea reconocida en nuestro contexto de persistencia, personapojo tiene que ser escanenada por Hibernate y como PersonaPOJO.java no es una entidad no sería analizada si la definimos en ella. Por este motivo la dejaremos en Persona:

@SqlResultSetMapping(name = "personapojo",
  classes = {
    @ConstructorResult(targetClass = PersonaPOJO.class,
      columns = {
        @ColumnResult(name = "nn", type = String.class),
        @ColumnResult(name = "aa", type = String.class)})})
@Entity
@Table(schema = "jpa", name = "persona")
public class Persona {
    @Id
    private int id;
    private String nombre;
    private String apellido;
}
4- persistence.xml

El archivo persistence.xml hay que dejarlo dentro de META-INF con el siguiente contenido:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.2"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="em"
                      transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
    </persistence-unit>
</persistence>
Definimos el nombre de nuestra unidad de persistencia (em) e indicamos con RESOURCE_LOCAL que nosotros administramos la creación de los EntityManager y que las transacciones serán administrados localmente y no por alguna implementacion JTA (es lo que necesitamos para nuestro ejemplo).

5- Contexto de Persistencia
Lo primero que hay que hacer es crear el EntityManagerFactory:
private static EntityManagerFactory emf
    = Persistence.createEntityManagerFactory("em", cargarPropiedades());

private static Map<String, String> cargarPropiedades() {
      return Map.of(
        "javax.persistence.jdbc.driver", System.getProperty("jdbc_driver"),
        "javax.persistence.jdbc.url", System.getProperty("jdbc_url"),
        "javax.persistence.jdbc.user", System.getProperty("jdbc_user"),
        "javax.persistence.jdbc.password", System.getProperty("jdbc_password"),
        "hibernate.hbm2ddl.auto", "validate"
      );
}

usando el metodo cargarPropiedades() definimos programaticamente como será el acceso a la base de datos y no utilizamos el archivo persistence.xml para este propósito.

Luego para obtener el EntityManager:
public static EntityManager obtener() {
 return emf.createEntityManager();
}


6- Test

Para los tests usaré el siguiente script que permitirá tener datos precargados:
create schema jpa;

create table jpa.persona (
   id int primary key,
   nombre varchar default null,
   apellido varchar default null
);

insert into jpa.persona (id, nombre, apellido) values (1, 'Sebastián', 'Ávila');
Ahora las pruebas... El objetivo es sacar los datos disponibles para Persona (puede ser cualquier dato, no es necesario que esté asociado a la entidad) y usarlos para construir una instancia de PersonaPOJO (no es @Entity) utilizando JPA:
// obtener el contexto de persistencia
final var em = ContextoPersistencia.obtener();

// obtener una entidad que ya existe en la base de datos (solo por entretención)
final var persona = em.find(Persona.class, 1);
validarPersona(persona);

// Método 1: utilizar ConstructorResult junto a NativeQuery
List<PersonaPOJO> personas = em
  .createNativeQuery("select nombre nn, apellido aa from jpa.persona", "personapojo")
  .getResultList();
assertThat(personas.size()).isEqualTo(1);
validarPersona(personas.get(0));

// Método 2: utilizando JPQL y el constructor del POJO para la proyección
personas = em
  .createQuery("select new com.sebastian.pojojpa.dominio.PersonaPOJO(p.nombre, p.apellido) from Persona p")
  .getResultList();
validarPersona(personas.get(0));

// Método 3: obtener un List<Object[]> y hacer la transformación “manual”
final List<Object[]> obj = em
  .createNativeQuery("select nombre nn, apellido aa from jpa.persona")
  .getResultList();
validarPersona(obj.get(0));

em.close();
Los métodos de validación:
private void validarPersona(final Object[] persona) {
    final var pj = new PersonaPOJO();
    pj.setNn((String) persona[0]);
    pj.setAa((String) persona[1]);
    validarPersona(pj);
}
    
private void validarPersona(final Persona p) {
    assertThat(p).isNotNull();
    assertThat(p.getId()).isEqualTo(1);
    assertThat(p.getNombre()).isEqualTo("Sebastián");
    assertThat(p.getApellido()).isEqualTo("Ávila");
}

private void validarPersona(final PersonaPOJO p) {
    assertThat(p).isNotNull();
    assertThat(p.getNn()).isEqualTo("Sebastián");
    assertThat(p.getAa()).isEqualTo("Ávila");
}

Estructura del proyecto:



Y eso es todo☺el código está disponible en GitHub para ver el detalle de la implementación ✌.