JPA: Cómo convertir un conjunto de resultados de consultas nativas a una colección de clases POJO

Estoy usando JPA en mi proyecto.

Llegué a una consulta en la que necesito hacer una operación de unión en cinco tablas. Así que creé una consulta nativa que devuelve cinco campos.

Ahora quiero convertir el objeto resultante a la clase POJO de java que contiene las mismas cinco cadenas.

¿Hay alguna forma en JPA para lanzar directamente ese resultado a la lista de objetos POJO?

Llegué a la siguiente solución …

@NamedNativeQueries({ @NamedNativeQuery( name = "nativeSQL", query = "SELECT * FROM Actors", resultClass = db.Actor.class), @NamedNativeQuery( name = "nativeSQL2", query = "SELECT COUNT(*) FROM Actors", resultClass = XXXXX) // <--------------- problem }) 

Ahora, aquí en resultClass, ¿necesitamos proporcionar una clase que sea una entidad JPA real? O ¿Podemos convertirlo a cualquier clase JAVA POJO que contenga los mismos nombres de columna?

JPA proporciona un SqlResultSetMapping que le permite asignar los retornos de su consulta nativa a una Entidad o una clase personalizada .

EDIT JPA 1.0 no permite el mapeo a clases que no sean de entidad. Solo en JPA 2.1 se ha agregado un ConstructorResult para asignar valores de retorno a una clase java.

Además, para el problema de OP con el recuento, debería ser suficiente para definir una asignación de conjunto de resultados con un solo ColumnResult

He encontrado un par de soluciones para esto.

Usar entidades mapeadas (JPA 2.0)

Usando JPA 2.0 no es posible mapear una consulta nativa a un POJO, solo se puede hacer con una entidad.

Por ejemplo:

 Query query = em.createNativeQuery("SELECT name,age FROM jedi_table", Jedi.class); @SuppressWarnings("unchecked") List items = (List) query.getResultList(); 

Pero en este caso, Jedi , debe ser una clase de entidad mapeada.

Una alternativa para evitar la advertencia no marcada aquí, sería usar una consulta nativa con nombre. Entonces, si declaramos la consulta nativa en una entidad

 @NamedNativeQuery( name="jedisQry", query = "SELECT name,age FROM jedis_table", resultClass = Jedi.class) 

Entonces, podemos simplemente hacer:

 TypedQuery query = em.createNamedQuery("jedisQry", Jedi.class); List items = query.getResultList(); 

Esto es más seguro, pero todavía estamos restringidos para usar una entidad mapeada.

Asignación manual

Una solución que experimenté un poco (antes de la llegada de JPA 2.1) fue hacer un mapeo contra un constructor de POJO usando un poco de reflexión.

 public static  T map(Class type, Object[] tuple){ List> tupleTypes = new ArrayList<>(); for(Object field : tuple){ tupleTypes.add(field.getClass()); } try { Constructor ctor = type.getConstructor(tupleTypes.toArray(new Class< ?>[tuple.length])); return ctor.newInstance(tuple); } catch (Exception e) { throw new RuntimeException(e); } } 

Este método básicamente toma una matriz de tuplas (como la devuelven las consultas nativas) y la asigna a una clase POJO proporcionada buscando un constructor que tenga el mismo número de campos y del mismo tipo.

Entonces podemos usar métodos convenientes como:

 public static  List map(Class type, List records){ List result = new LinkedList<>(); for(Object[] record : records){ result.add(map(type, record)); } return result; } public static  List getResultList(Query query, Class type){ @SuppressWarnings("unchecked") List records = query.getResultList(); return map(type, records); } 

Y podemos simplemente usar esta técnica de la siguiente manera:

 Query query = em.createNativeQuery("SELECT name,age FROM jedis_table"); List jedis = getResultList(query, Jedi.class); 

JPA 2.1 con @SqlResultSetMapping

Con la llegada de JPA 2.1, podemos usar la anotación @SqlResultSetMapping para resolver el problema.

Necesitamos declarar un mapeo de conjunto de resultados en alguna parte de una entidad:

 @SqlResultSetMapping(name="JediResult", classes = { @ConstructorResult(targetClass = Jedi.class, columns = {@ColumnResult(name="name"), @ColumnResult(name="age")}) }) 

Y luego simplemente hacemos:

 Query query = em.createNativeQuery("SELECT name,age FROM jedis_table", "JediResult"); @SuppressWarnings("unchecked") List samples = query.getResultList(); 

Por supuesto, en este caso Jedi no necesita ser una entidad mapeada. Puede ser un POJO regular.

Usando mapeo XML

Soy uno de los que encuentran que agregar todos estos @SqlResultSetMapping bastante invasivo en mis entidades, y particularmente me desagrada la definición de consultas con nombre dentro de las entidades, así que alternativamente hago todo esto en el archivo META-INF/orm.xml :

  SELECT name,age FROM jedi_table        

Y esas son todas las soluciones que conozco. Los dos últimos son la forma ideal si podemos usar JPA 2.1.

Sí, con JPA 2.1 es fácil. Tienes Anotaciones muy útiles. Simplifican tu vida.

Primero declare su consulta nativa, luego la asignación de conjunto de resultados (que define la asignación de los datos devueltos por la base de datos a sus POJO). Escriba su clase de POJO para referirse a (no se incluye aquí por brevedad). Por último, pero no por eso menos importante: cree un método en un DAO (por ejemplo) para llamar a la consulta. Esto funcionó para mí en una aplicación dropwizard (1.0.0).

Primero declara una consulta nativa en una clase de entidad:

 @NamedNativeQuery ( name = "domain.io.MyClass.myQuery", query = "Select a.colA, a.colB from Table a", resultSetMapping = "mappinMyNativeQuery") // must be the same name as in the SqlResultSetMapping declaration 

Debajo, puede agregar la statement de asignación del conjunto de resultados:

 @SqlResultSetMapping( name = "mapppinNativeQuery", // same as resultSetMapping above in NativeQuery classes = { @ConstructorResult( targetClass = domain.io.MyMapping.class columns = { @ColumnResult( name = "colA", type = Long.class), @ColumnResult( name = "colB", type = String.class) } ) } ) 

Más adelante en un DAO puede referirse a la consulta como

 public List findAll() { return (namedQuery("domain.io.MyClass.myQuery").list()); } 

Eso es.

Primero declara las siguientes anotaciones:

 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface NativeQueryResultEntity { } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface NativeQueryResultColumn { int index(); } 

Luego anota tu POJO de la siguiente manera:

 @NativeQueryResultEntity public class ClassX { @NativeQueryResultColumn(index=0) private String a; @NativeQueryResultColumn(index=1) private String b; } 

Luego escribe el procesador de anotaciones:

 public class NativeQueryResultsMapper { private static Logger log = LoggerFactory.getLogger(NativeQueryResultsMapper.class); public static  List map(List objectArrayList, Class genericType) { List ret = new ArrayList(); List mappingFields = getNativeQueryResultColumnAnnotatedFields(genericType); try { for (Object[] objectArr : objectArrayList) { T t = genericType.newInstance(); for (int i = 0; i < objectArr.length; i++) { BeanUtils.setProperty(t, mappingFields.get(i).getName(), objectArr[i]); } ret.add(t); } } catch (InstantiationException ie) { log.debug("Cannot instantiate: ", ie); ret.clear(); } catch (IllegalAccessException iae) { log.debug("Illegal access: ", iae); ret.clear(); } catch (InvocationTargetException ite) { log.debug("Cannot invoke method: ", ite); ret.clear(); } return ret; } // Get ordered list of fields private static  List getNativeQueryResultColumnAnnotatedFields(Class genericType) { Field[] fields = genericType.getDeclaredFields(); List orderedFields = Arrays.asList(new Field[fields.length]); for (int i = 0; i < fields.length; i++) { if (fields[i].isAnnotationPresent(NativeQueryResultColumn.class)) { NativeQueryResultColumn nqrc = fields[i].getAnnotation(NativeQueryResultColumn.class); orderedFields.set(nqrc.index(), fields[i]); } } return orderedFields; } } 

Use el marco anterior de la siguiente manera:

 String sql = "select a,b from x order by a"; Query q = entityManager.createNativeQuery(sql); List results = NativeQueryResultsMapper.map(q.getResultList(), ClassX.class); 

Si usa Spring-jpa , este es un suplemento de las respuestas y esta pregunta. Por favor, corrige esto si hay algún defecto. Principalmente he usado tres métodos para lograr “mapear el resultado Object[] a un pojo” en función de la necesidad práctica que encuentro:

  1. El método JPA incorporado es suficiente.
  2. El método JPA incorporado no es suficiente, pero un sql personalizado con su Entity es suficiente.
  3. Los primeros 2 fallaron, y tengo que usar un idioma nativeQuery . Aquí están los ejemplos. El pojo esperado:

     public class Antistealingdto { private String secretKey; private Integer successRate; // GETTERs AND SETTERs public Antistealingdto(String secretKey, Integer successRate) { this.secretKey = secretKey; this.successRate = successRate; } } 

Método 1 : Cambie el pojo en una interfaz:

 public interface Antistealingdto { String getSecretKey(); Integer getSuccessRate(); } 

Y repository

 interface AntiStealingRepository extends CrudRepository { Antistealingdto findById(Long id); } 

Método 2 : Repositorio:

 @Query("select new AntistealingDTO(secretKey, successRate) from Antistealing where ....") Antistealing whatevernamehere(conditions); 

Nota: la secuencia de parámetros del constructor POJO debe ser idéntica tanto en la definición POJO como en sql.

Método 3 : utilice @SqlResultSetMapping y @NamedNativeQuery en Entity como ejemplo en la respuesta de Edwin Dalorzo.

Los primeros dos métodos llamarían a muchos manejadores en el medio, como convertidores personalizados. Por ejemplo, AntiStealing define una AntiStealing , antes de que persista, se inserta un convertidor para encriptarlo. Esto daría como resultado los primeros 2 métodos devolviendo una secretKey convertida que no es lo que quiero. Mientras que el método 3 superaría el convertidor, y devuelto secretKey sería el mismo que está almacenado (uno encriptado).

si está utilizando Spring, puede usar org.springframework.jdbc.core.RowMapper

Aquí hay un ejemplo:

 public List query(String objectType, String namedQuery) { String rowMapper = objectType + "RowMapper"; // then by reflection you can instantiate and use. The RowMapper classes need to follow the naming specific convention to follow such implementation. } 

Como otros ya han mencionado todas las posibles soluciones, estoy compartiendo mi solución alternativa.

En mi situación con Postgres 9.4 , mientras trabajaba con Jackson ,

 //Convert it to named native query. List list = em.createNativeQuery("select cast(array_to_json(array_agg(row_to_json(a))) as text) from myschema.actors a") .getResultList(); List map = new ObjectMapper().readValue(list.get(0), new TypeReference>() {}); 

Estoy seguro de que puedes encontrar lo mismo para otras bases de datos.

También FYI, resultados de la consulta nativa JPA 2.0 como mapa

El procedimiento de desenvolver se puede realizar para asignar resultados a entidades que no sean entidades (que es Beans / POJO). El procedimiento es el siguiente.

 List dtoList = entityManager.createNativeQuery(sql) .setParameter("userId", userId) .unwrap(org.hibernate.Query.class).setResultTransformer(Transformers.aliasToBean(JobDTO.class)).list(); 

El uso es para la implementación de JPA-Hibernate.

Utilice el DTO Design Pattern . Fue utilizado en EJB 2.0 . La entidad fue administrada por contenedor. DTO Design Pattern se usa para resolver este problema. Pero, podría ser útil ahora, cuando la aplicación se desarrolle Server Side y Client Side separado. DTO se usa cuando el Server side no quiere pasar / devolver Entity con anotación al Client Side .

Ejemplo de DTO:

PersonEntity.java

 @Entity public class PersonEntity { @Id private String id; private String address; public PersonEntity(){ } public PersonEntity(String id, String address) { this.id = id; this.address = address; } //getter and setter } 

PersonDTO.java

 public class PersonDTO { private String id; private String address; public PersonDTO() { } public PersonDTO(String id, String address) { this.id = id; this.address = address; } //getter and setter } 

DTOBuilder.java

 public class DTOBuilder() { public static PersonDTO buildPersonDTO(PersonEntity person) { return new PersonDTO(person.getId(). person.getAddress()); } } 

EntityBuilder.java < - it mide ser necesario

 public class EntityBuilder() { public static PersonEntity buildPersonEntity(PersonDTO person) { return new PersonEntity(person.getId(). person.getAddress()); } } 

Consulte el ejemplo a continuación para usar un POJO como pseudo entidad para recuperar el resultado de la consulta nativa sin utilizar SqlResultSetMapping complejo. Solo necesita dos anotaciones, una @Enity desnuda y una @Id dummy en su POJO. @Id se puede usar en cualquier campo de su elección, un campo @Id puede tener claves duplicadas pero no valores nulos.

Dado que @Enity no se correlaciona con ninguna tabla física, este POJO se denomina pseudo entidad.

Entorno: eclipselink 2.5.0-RC1, jpa-2.1.0, mysql-connector-java-5.1.14

Puede descargar el proyecto maven completo aquí

La consulta nativa se basa en los empleados de la muestra mysql db http://dev.mysql.com/doc/employee/en/employees-installation.html

persistence.xml

 < ?xml version="1.0" encoding="UTF-8"?>  org.moonwave.jpa.model.pojo.Employee        

Empleado.java

 package org.moonwave.jpa.model.pojo; @Entity public class Employee { @Id protected Long empNo; protected String firstName; protected String lastName; protected String title; public Long getEmpNo() { return empNo; } public void setEmpNo(Long empNo) { this.empNo = empNo; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String toString() { StringBuilder sb = new StringBuilder(); sb.append("empNo: ").append(empNo); sb.append(", firstName: ").append(firstName); sb.append(", lastName: ").append(lastName); sb.append(", title: ").append(title); return sb.toString(); } } 

EmployeeNativeQuery.java

 public class EmployeeNativeQuery { private EntityManager em; private EntityManagerFactory emf; public void setUp() throws Exception { emf=Persistence.createEntityManagerFactory("jpa-mysql"); em=emf.createEntityManager(); } public void tearDown()throws Exception { em.close(); emf.close(); } @SuppressWarnings("unchecked") public void query() { Query query = em.createNativeQuery("select e.emp_no as empNo, e.first_name as firstName, e.last_name as lastName," + "t.title from employees e join titles t on e.emp_no = t.emp_no", Employee.class); query.setMaxResults(30); List list = (List) query.getResultList(); int i = 0; for (Object emp : list) { System.out.println(++i + ": " + emp.toString()); } } public static void main( String[] args ) { EmployeeNativeQuery test = new EmployeeNativeQuery(); try { test.setUp(); test.query(); test.tearDown(); } catch (Exception e) { System.out.println(e); } } } 

Una forma simple de convertir consultas SQL a la colección de clases POJO,

 Query query = getCurrentSession().createSQLQuery(sqlQuery).addEntity(Actors.class); List list = (List) query.list(); return list;