He encontrado un par de soluciones para esto.
Uso de entidades mapeadas (JPA 2.0)
Con JPA 2.0 no es posible asignar 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<Jedi> items = (List<Jedi>) query.getResultList();
Pero en este caso, Jedi
debe ser una clase de entidad asignada.
Una alternativa para evitar la advertencia no verificada aquí sería utilizar 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, simplemente podemos hacer:
TypedQuery<Jedi> query = em.createNamedQuery("jedisQry", Jedi.class);
List<Jedi> items = query.getResultList();
Esto es más seguro, pero todavía estamos restringidos a usar una entidad asignada.
Mapeo manual
Una solución que experimenté un poco (antes de la llegada de JPA 2.1) fue hacer un mapeo contra un constructor POJO usando un poco de reflexión.
public static <T> T map(Class<T> type, Object[] tuple){
List<Class<?>> tupleTypes = new ArrayList<>();
for(Object field : tuple){
tupleTypes.add(field.getClass());
}
try {
Constructor<T> ctor = type.getConstructor(tupleTypes.toArray(new Class<?>[tuple.length]));
return ctor.newInstance(tuple);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Básicamente, este método 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 <T> List<T> map(Class<T> type, List<Object[]> records){
List<T> result = new LinkedList<>();
for(Object[] record : records){
result.add(map(type, record));
}
return result;
}
public static <T> List<T> getResultList(Query query, Class<T> type){
@SuppressWarnings("unchecked")
List<Object[]> records = query.getResultList();
return map(type, records);
}
Y simplemente podemos usar esta técnica de la siguiente manera:
Query query = em.createNativeQuery("SELECT name,age FROM jedis_table");
List<Jedi> 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 una asignación de conjunto de resultados en algún lugar 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<Jedi> samples = query.getResultList();
Por supuesto, en este caso Jedi
no necesita ser una entidad mapeada. Puede ser un POJO regular.
Usar mapeo XML
Soy uno de los que encuentran que agregar todo esto es @SqlResultSetMapping
bastante invasivo en mis entidades, y particularmente no me gusta la definición de consultas con nombre dentro de las entidades, por lo que alternativamente hago todo esto en el META-INF/orm.xml
archivo:
<named-native-query name="GetAllJedi" result-set-mapping="JediMapping">
<query>SELECT name,age FROM jedi_table</query>
</named-native-query>
<sql-result-set-mapping name="JediMapping">
<constructor-result target-class="org.answer.model.Jedi">
<column name="name" class="java.lang.String"/>
<column name="age" class="java.lang.Integer"/>
</constructor-result>
</sql-result-set-mapping>
Y esas son todas las soluciones que conozco. Los dos últimos son la forma ideal si podemos usar JPA 2.1.