web-dev-qa-db-fra.com

JPA: Comment convertir un jeu de résultats de requête natif en collection de classes POJO

J'utilise JPA dans mon projet.

Je suis arrivé à une requête dans laquelle je dois faire l'opération de jointure sur cinq tables. J'ai donc créé une requête native qui renvoie cinq champs.

Maintenant, je veux convertir l'objet de résultat en classe Java POJO qui contient les cinq mêmes chaînes.

Existe-t-il un moyen dans JPA de convertir directement ce résultat en liste d'objets POJO?

Je suis venu à la solution suivante ..

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

Maintenant, ici, dans resultClass, devons-nous fournir une classe qui est une entité JPA réelle? OR Nous pouvons la convertir en n'importe quelle classe Java POJO contenant les mêmes noms de colonne?

140
Gunjan Shah

JPA fournit une SqlResultSetMapping qui vous permet de mapper tout ce qui retourne de votre requête native dans une entité ou une classe personnalisée.

EDIT JPA 1.0 n'autorise pas le mappage vers des classes non-entités. Uniquement dans JPA 2.1, un ConstructorResult a été ajouté pour mapper les valeurs de retour d'une classe Java.

En outre, pour que OP ait le problème d'obtenir le nombre, il devrait suffire de définir un mappage d'ensemble de résultats avec un seul ColumnResult

84
Denis Tulskiy

J'ai trouvé quelques solutions à cela. 

Utilisation d'entités mappées (JPA 2.0)

Avec JPA 2.0, il n'est pas possible de mapper une requête native sur un POJO, cela ne peut être fait qu'avec une entité.

Par exemple:

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

Mais dans ce cas, Jedi, doit être une classe d'entités mappée.

Une alternative pour éviter l'avertissement non contrôlé ici serait d'utiliser une requête native nommée. Donc, si nous déclarons la requête native dans une entité

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

Ensuite, nous pouvons simplement faire:

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

C'est plus sûr, mais nous sommes toujours limités à utiliser une entité mappée.

Cartographie manuelle

Une solution que j'ai un peu expérimentée (avant l’arrivée de JPA 2.1) consistait à mapper sur un constructeur POJO en utilisant un peu de réflexion.

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);
   }
}

Cette méthode prend essentiellement un tableau Tuple (tel que retourné par les requêtes natives) et le mappe avec une classe POJO fournie en recherchant un constructeur qui a le même nombre de champs et du même type.

Ensuite, nous pouvons utiliser des méthodes pratiques comme:

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);
}

Et nous pouvons simplement utiliser cette technique comme suit:

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

JPA 2.1 avec @SqlResultSetMapping

Avec l'arrivée de JPA 2.1, nous pouvons utiliser l'annotation @SqlResultSetMapping pour résoudre le problème. 

Nous devons déclarer un mappage de jeu de résultats quelque part dans une entité:

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

Et puis on fait simplement:

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

Bien entendu, dans ce cas, Jedi n'a pas besoin d'être une entité mappée. Ce peut être un POJO ordinaire.

Utilisation du mappage XML

Je suis un de ceux qui trouvent l'ajout de tous ces @SqlResultSetMapping assez invasif dans mes entités, et je n'aime pas particulièrement la définition des requêtes nommées au sein d'entités, alors je fais tout cela dans le fichier META-INF/orm.xml:

<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>

Et ce sont toutes les solutions que je connais. Les deux derniers sont le moyen idéal si nous pouvons utiliser JPA 2.1.

171
Edwin Dalorzo

Oui, avec JPA 2.1, c'est facile. Vous avez des annotations très utiles. Ils vous simplifient la vie.

Commencez par déclarer votre requête native, puis votre mappage de jeu de résultats (qui définit le mappage des données renvoyées par la base de données à vos POJO). Ecrivez votre classe POJO à laquelle faire référence (non incluse ici pour la brièveté). Dernier point mais non le moindre: créez une méthode dans un DAO (par exemple) pour appeler la requête. Cela a fonctionné pour moi dans une application Dropwizard (1.0.0).

Commencez par déclarer une requête native dans une classe d'entité:

@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

En dessous, vous pouvez ajouter la déclaration de mappage de jeu de résultats:

@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)
          }
      )
   } 
)

Plus tard dans un DAO, vous pouvez vous référer à la requête en tant que

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

C'est tout.

9
John

Si vous utilisez Spring-jpa, ceci est un complément aux réponses et à cette question. S'il vous plaît corriger cela si des défauts. J'ai principalement utilisé trois méthodes pour "associer le résultat Object[] à un pojo" en fonction de mes besoins pratiques:

  1. La méthode intégrée JPA suffit.
  2. La méthode intégrée JPA ne suffit pas, mais une sql personnalisée avec sa Entity suffit.
  3. L'ancien 2 a échoué et je dois utiliser une nativeQuery. Voici les exemples . Le pojo attendu:

    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éthode 1 : Change le pojo en une interface:

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

Et référentiel:

interface AntiStealingRepository extends CrudRepository<Antistealing, Long> {
    Antistealingdto findById(Long id);
}

Méthode 2 : Référentiel:

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

Remarque: la séquence de paramètres du constructeur POJO doit être identique dans la définition de POJO et dans SQL.

Méthode 3 : Utilisez @SqlResultSetMapping et @NamedNativeQuery dans Entity comme exemple dans la réponse d'Edwin Dalorzo. 

Les deux premières méthodes appellent de nombreux gestionnaires du milieu, tels que des convertisseurs personnalisés. Par exemple, AntiStealing définit une secretKey; avant sa persistance, un convertisseur est inséré pour le chiffrer. Cela aurait pour conséquence que les 2 premières méthodes renverraient une secretKey reconvertie, ce qui n'est pas ce que je veux. Alors que la méthode 3 vaincrait le convertisseur, secretKey retourné serait le même qu’il est stocké (chiffré).

7
Tiina

Commencez par déclarer les annotations suivantes:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface NativeQueryResultEntity {
}

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

Puis annotez votre POJO comme suit:

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

    @NativeQueryResultColumn(index=1)
    private String b;
}

Puis écrivez processeur d'annotation:

public class NativeQueryResultsMapper {

    private static Logger log = LoggerFactory.getLogger(NativeQueryResultsMapper.class);

    public static <T> List<T> map(List<Object[]> objectArrayList, Class<T> genericType) {
        List<T> ret = new ArrayList<T>();
        List<Field> 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 <T> List<Field> getNativeQueryResultColumnAnnotatedFields(Class<T> genericType) {
        Field[] fields = genericType.getDeclaredFields();
        List<Field> 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;
    }
}

Utilisez le cadre ci-dessus comme suit:

String sql = "select a,b from x order by a";
Query q = entityManager.createNativeQuery(sql);

List<ClassX> results = NativeQueryResultsMapper.map(q.getResultList(), ClassX.class);
4
riship89

Une procédure de déballage peut être effectuée pour affecter des résultats à des entités autres que des entités (Beans/POJO). La procédure est la suivante.

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

L'utilisation est pour l'implémentation JPA-Hibernate.

3
zawhtut

En veille prolongée, vous pouvez utiliser ce code pour mapper facilement votre requête native.

private List < Map < String, Object >> getNativeQueryResultInMap() {
String mapQueryStr = "SELECT * FROM AB_SERVICE three ";
Query query = em.createNativeQuery(mapQueryStr);
NativeQueryImpl nativeQuery = (NativeQueryImpl) query;
nativeQuery.setResultTransformer(AliasToEntityMapResultTransformer.INSTANCE);
List < Map < String, Object >> result = query.getResultList();
for (Map map: result) {
    System.out.println("after request  ::: " + map);
}
return result;}

Ancien style en utilisant Resultset

@Transactional(readOnly=true)
public void accessUser() {
    EntityManager em = this.getEntityManager();
    org.hibernate.Session session = em.unwrap(org.hibernate.Session.class);
    session.doWork(new Work() {
        @Override
        public void execute(Connection con) throws SQLException {
            try (PreparedStatement stmt = con.prepareStatement(
                    "SELECT u.username, u.name, u.email, 'blabla' as passe, login_type as loginType FROM users u")) {
                ResultSet rs = stmt.executeQuery();
                ResultSetMetaData rsmd = rs.getMetaData();
                for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                    System.out.print(rsmd.getColumnName(i) + " (" + rsmd.getColumnTypeName(i) + ") / ");
                }
                System.out.println("");
                while (rs.next()) {
                    System.out.println("Found username " + rs.getString("USERNAME") + " name " + rs.getString("NAME") + " email " + rs.getString("EMAIL") + " passe " + rs.getString("PASSE") + " email " + rs.getInt("LOGIN_TYPE"));
                }
            }
        }
    });
}
1
Rubens

Utiliser Hibernate:

@Transactional(readOnly=true)
public void accessUser() {
EntityManager em = repo.getEntityManager();
    org.hibernate.Session session = em.unwrap(org.hibernate.Session.class);
    org.hibernate.SQLQuery q = (org.hibernate.SQLQuery) session.createSQLQuery("SELECT u.username, u.name, u.email, 'blabla' as passe, login_type as loginType FROM users u").addScalar("username", StringType.INSTANCE).addScalar("name", StringType.INSTANCE).addScalar("email", StringType.INSTANCE).addScalar("passe", StringType.INSTANCE).addScalar("loginType", IntegerType.INSTANCE)
        .setResultTransformer(Transformers.aliasToBean(User2DTO.class));

    List<User2DTO> userList = q.list();
}
1
Rubens

Tout ce dont vous avez besoin est un DTO avec un constructeur:

public class User2DTO implements Serializable {

    /** pode ser email ou id do Google comecando com G ou F para Facebook */
    private String username;

    private String password;

    private String email;

    private String name;

    private Integer loginType;

    public User2DTO(Object...fields) {
        super();
        this.username = (String) fields[0];
        this.name = (String) fields[1];
        this.email = (String) fields[2];
        this.password = (String) fields[3];
        this.loginType = (Integer) fields[4];
    }

et appelez ça:

EntityManager em = repo.getEntityManager();
        Query q = em.createNativeQuery("SELECT u.username, u.name, u.email, 'blabla' as passe, login_type as loginType FROM users u");
        List<Object[]> objList = q.getResultList();
        List<User2DTO> ooBj = objList.stream().map(User2DTO::new).collect(Collectors.toList());
0
Rubens

Le moyen le plus simple consiste à utiliser so projections . Il peut mapper les résultats de la requête directement aux interfaces et est plus facile à mettre en œuvre que l'utilisation de SqlResultSetMapping.

Un exemple est présenté ci-dessous:

@Repository
public interface PeopleRepository extends JpaRepository<People, Long> {

    @Query(value = "SELECT p.name AS name, COUNT(dp.people_id) AS count " +
        "FROM people p INNER JOIN dream_people dp " +
        "ON p.id = dp.people_id " +
        "WHERE p.user_id = :userId " +
        "GROUP BY dp.people_id " +
        "ORDER BY p.name", nativeQuery = true)
    List<PeopleDTO> findByPeopleAndCountByUserId(@Param("userId") Long userId);

    @Query(value = "SELECT p.name AS name, COUNT(dp.people_id) AS count " +
        "FROM people p INNER JOIN dream_people dp " +
        "ON p.id = dp.people_id " +
        "WHERE p.user_id = :userId " +
        "GROUP BY dp.people_id " +
        "ORDER BY p.name", nativeQuery = true)
    Page<PeopleDTO> findByPeopleAndCountByUserId(@Param("userId") Long userId, Pageable pageable);

}



// Interface to which result is projected
public interface PeopleDTO {

    String getName();

    Long getCount();

}

Les champs de l'interface projetée doivent correspondre aux champs de cette entité. Sinon, le mappage des champs pourrait se rompre.

De même, si vous utilisez la notation SELECT table.column, définissez toujours des alias correspondant aux noms d'entité, comme indiqué dans l'exemple.

0
Thanthu

Puisque d'autres ont déjà mentionné toutes les solutions possibles, je partage ma solution de contournement.

Dans ma situation avec Postgres 9.4, tout en travaillant avec Jackson,

//Convert it to named native query.
List<String> list = em.createNativeQuery("select cast(array_to_json(array_agg(row_to_json(a))) as text) from myschema.actors a")
                   .getResultList();

List<ActorProxy> map = new ObjectMapper().readValue(list.get(0), new TypeReference<List<ActorProxy>>() {});

Je suis sûr que vous pouvez trouver la même chose pour d'autres bases de données.

Pour votre information, les résultats de la requête native JPA 2.0 sous forme de carte

0
Darshan Patel

Ancien style utilisant ResultSet

@Transactional(readOnly=true)
public void accessUser() {
    EntityManager em = this.getEntityManager();
    org.hibernate.Session session = em.unwrap(org.hibernate.Session.class);
    session.doWork(new Work() {
        @Override
        public void execute(Connection con) throws SQLException {
            try (PreparedStatement stmt = con.prepareStatement(
                    "SELECT u.username, u.name, u.email, 'blabla' as passe, login_type as loginType FROM users u")) {
                ResultSet rs = stmt.executeQuery();
                ResultSetMetaData rsmd = rs.getMetaData();
                for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                    System.out.print(rsmd.getColumnName(i) + " (" + rsmd.getColumnTypeName(i) + ") / ");
                }
                System.out.println("");
                while (rs.next()) {
                    System.out.println("Found username " + rs.getString("USERNAME") + " name " + rs.getString("NAME") + " email " + rs.getString("EMAIL") + " passe " + rs.getString("PASSE") + " email " + rs.getInt("LOGIN_TYPE"));
                }
            }
        }
    });
}
0
Rubens

si vous utilisez Spring, vous pouvez utiliser org.springframework.jdbc.core.RowMapper

Voici un exemple: 

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.
} 
0
Pallab Rath

Utiliser Hibernate:

@Transactional(readOnly=true)
public void accessUser() {
    EntityManager em = repo.getEntityManager();
    org.hibernate.Session session = em.unwrap(org.hibernate.Session.class);
    org.hibernate.SQLQuery q = (org.hibernate.SQLQuery) session.createSQLQuery("SELECT u.username, u.name, u.email, 'blabla' as passe, login_type as loginType FROM users u")
        .addScalar("username", StringType.INSTANCE).addScalar("name", StringType.INSTANCE)
        .addScalar("email", StringType.INSTANCE).addScalar("passe", StringType.INSTANCE)
        .addScalar("loginType", IntegerType.INSTANCE)
        .setResultTransformer(Transformers.aliasToBean(User2DTO.class));

    List<User2DTO> userList = q.list();
}
0
Rubens

Je ne sais pas si cela convient ici, mais j'avais une question similaire et j'ai trouvé la solution/exemple simple suivant:

private EntityManager entityManager;
...
    final String sql = " SELECT * FROM STORE "; // select from the table STORE
    final Query sqlQuery = entityManager.createNativeQuery(sql, Store.class);

    @SuppressWarnings("unchecked")
    List<Store> results = (List<Store>) sqlQuery.getResultList();

Dans mon cas, je devais utiliser des parties SQL définies dans Strings ailleurs, donc je ne pouvais pas simplement utiliser NamedNativeQuery.

0
Andreas L.

Nous avons résolu le problème en utilisant la méthode suivante:

   //Add actual table name here in Query
    final String sqlQuery = "Select a.* from ACTORS a"
    // add your entity manager here 
    Query query = entityManager.createNativeQuery(sqlQuery,Actors.class);
    //List contains the mapped entity data.
    List<Actors> list = (List<Actors>) query.getResultList();
0
Akash