wake-up-neo.com

JPA: Konvertieren einer systemeigenen Abfrageergebnisgruppe in eine POJO-Klassensammlung

Ich verwende JPA in meinem Projekt.

Ich kam zu einer Abfrage, in der ich Join-Operationen an fünf Tabellen vornehmen muss. Also habe ich eine native Abfrage erstellt, die fünf Felder zurückgibt.

Jetzt möchte ich das Ergebnisobjekt in die Java-POJO-Klasse konvertieren, die die gleichen fünf Strings enthält.

Gibt es eine Möglichkeit in JPA, dieses Ergebnis direkt in die POJO-Objektliste umzuwandeln?

Ich bin zu folgender Lösung gekommen ..

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

Müssen wir hier in resultClass eine Klasse bereitstellen, die eine tatsächliche JPA-Entität ist? OR Wir können sie in eine beliebige Java-POJO-Klasse konvertieren, die dieselben Spaltennamen enthält.

140
Gunjan Shah

JPA stellt eine SqlResultSetMapping zur Verfügung, mit der Sie die von Ihrer systemeigenen Abfrage zurückgegebenen Ergebnisse in einer Entität abbilden können oder eine benutzerdefinierte Klasse.

EDIT JPA 1.0 erlaubt keine Zuordnung zu Nicht-Entitätsklassen. Nur in JPA 2.1 wurde ein ConstructorResult hinzugefügt, um Rückgabewerte einer Java-Klasse zuzuordnen.

Für OPs Probleme beim Abrufen der Zählung sollte es ausreichend sein, eine Ergebnismengenzuordnung mit einer einzelnen ColumnResult zu definieren.

84
Denis Tulskiy

Ich habe dafür ein paar Lösungen gefunden. 

Verwenden von zugeordneten Entitäten (JPA 2.0)

Mit JPA 2.0 ist es nicht möglich, eine native Abfrage einem POJO zuzuordnen. Dies kann nur mit einer Entität erfolgen.

Zum Beispiel:

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

In diesem Fall muss jedoch Jedi eine zugeordnete Entitätsklasse sein.

Eine Alternative zur Vermeidung der ungeprüften Warnung wäre die Verwendung einer benannten systemeigenen Abfrage. Wenn wir also die native Abfrage in einer Entität deklarieren

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

Dann können wir einfach tun:

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

Dies ist sicherer, aber wir dürfen nur eine zugeordnete Entität verwenden.

Manuelle Zuordnung

Eine Lösung, die ich ein wenig experimentiert hatte (vor der Ankunft von JPA 2.1), führte ein Mapping gegen einen POJO-Konstruktor mit etwas Reflexion durch.

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

Diese Methode verwendet im Wesentlichen ein Tuple-Array (wie von nativen Abfragen zurückgegeben) und ordnet es einer bereitgestellten POJO-Klasse zu, indem nach einem Konstruktor gesucht wird, der die gleiche Anzahl von Feldern und denselben Typ hat.

Dann können wir bequeme Methoden verwenden wie:

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

Und wir können diese Technik einfach wie folgt anwenden:

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

JPA 2.1 mit @SqlResultSetMapping

Mit der Ankunft von JPA 2.1 können wir die Annotation @SqlResultSetMapping verwenden, um das Problem zu lösen. 

Wir müssen eine Ergebnismengenzuordnung irgendwo in einer Entität deklarieren:

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

Und dann machen wir einfach:

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

Natürlich muss Jedi keine zugeordnete Entität sein. Es kann ein regulärer POJO sein.

XML-Zuordnung verwenden

Ich bin einer von denen, die das Hinzufügen all dieser @SqlResultSetMappings in meine Entitäten ziemlich invasiv finden, und ich mag die Definition benannter Abfragen innerhalb von Entitäten nicht, also tue ich das alles in der META-INF/orm.xml-Datei:

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

Und das sind alle Lösungen, die ich kenne. Die letzten beiden sind der ideale Weg, wenn wir JPA 2.1 verwenden können.

171
Edwin Dalorzo

Ja, mit JPA 2.1 ist es einfach. Sie haben sehr nützliche Anmerkungen. Sie vereinfachen dein Leben.

Deklarieren Sie zuerst Ihre native Abfrage und dann Ihre Ergebnissatzzuordnung (die die Zuordnung der von der Datenbank zurückgegebenen Daten zu Ihren POJOs definiert). Schreiben Sie Ihre POJO-Klasse, auf die Sie sich beziehen möchten (hier aus Gründen der Kürze nicht enthalten). Zu guter Letzt: Erstellen Sie eine Methode in einem DAO (zum Beispiel), um die Abfrage aufzurufen. Dies funktionierte für mich in einer Dropwizard (1.0.0) App.

Deklarieren Sie zuerst eine native Abfrage in einer Entitätsklasse:

@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

Darunter können Sie die Resultset-Mapping-Deklaration hinzufügen:

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

In einer DAO können Sie später auf die Abfrage als verweisen

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

Das ist es.

9
John

Wenn Sie Spring-jpa verwenden, ist dies eine Ergänzung zu den Antworten und dieser Frage. Bitte korrigieren Sie dies, wenn Sie Mängel haben. Ich habe hauptsächlich drei Methoden verwendet, um "Mapping-Ergebnis Object[] zu einem Pojo" zu erreichen, basierend auf dem praktischen Bedarf, den ich erfülle:

  1. Die in JPA eingebaute Methode reicht aus.
  2. Die in JPA eingebaute Methode reicht nicht aus, aber eine angepasste sql mit ihrer Entity reicht aus.
  3. Die erste 2 ist fehlgeschlagen, und ich muss eine nativeQuery..__ verwenden. Hier sind die Beispiele . Der Pojo erwartet:

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

Methode 1 : Ändere den Pojo in eine Schnittstelle:

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

Und Repository:

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

Methode 2 : Repository:

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

Hinweis: Die Parameterfolge des POJO-Konstruktors muss in der POJO-Definition und in der SQL-Anweisung identisch sein.

Methode 3 : Verwenden Sie @SqlResultSetMapping und @NamedNativeQuery in Entity als Beispiel in der Antwort von Edwin Dalorzo. 

Die ersten beiden Methoden würden viele in-the-middle-Handler aufrufen, z. B. angepasste Konverter. Beispielsweise definiert AntiStealing eine secretKey. Bevor sie dauerhaft gespeichert wird, wird ein Konverter eingefügt, um sie zu verschlüsseln. Dies würde dazu führen, dass die ersten beiden Methoden eine konvertierte secretKey zurückgeben, was ich nicht will. Während Methode 3 den Konverter überwinden würde, wäre secretKey dasselbe wie das gespeicherte (ein verschlüsselter).

7
Tiina

Erklären Sie zunächst folgende Anmerkungen:

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

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

Dann kommentieren Sie Ihren POJO wie folgt:

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

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

Dann schreiben Sie den Anmerkungsprozessor:

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

Verwenden Sie das obige Framework wie folgt:

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

Das Unwrap-Verfahren kann ausgeführt werden, um Ergebnisse an Nicht-Entitäten (Beans/POJO) zuzuweisen. Das Verfahren ist wie folgt.

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

Die Verwendung erfolgt für die JPA-Hibernate-Implementierung.

3
zawhtut

Im Ruhezustand können Sie diesen Code verwenden, um Ihre systemeigene Abfrage einfach zuzuordnen.

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

Alter Stil mit 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

Verwenden des Ruhezustands:

@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

Alles was Sie brauchen ist ein DTO mit einem Konstruktor:

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

und nenne es:

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

Am einfachsten ist es, so Projektionen zu verwenden. Es kann Abfrageergebnisse direkt auf Schnittstellen abbilden und ist einfacher zu implementieren als SqlResultSetMapping.

Ein Beispiel ist unten gezeigt:

@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();

}

Die Felder der projizierten Schnittstelle müssen mit den Feldern in dieser Entität übereinstimmen. Andernfalls könnte die Feldzuordnung unterbrochen werden.

Wenn Sie die SELECT table.column-Notation verwenden, definieren Sie immer Aliase, die mit den Namen der Entität übereinstimmen (siehe Beispiel).

0
Thanthu

Da andere bereits alle möglichen Lösungen erwähnt haben, teile ich meine Lösung für die Problemumgehung mit.

In meiner Situation mit Postgres 9.4, während Sie mit Jackson arbeiten,

//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>>() {});

Ich bin sicher, dass Sie das auch für andere Datenbanken finden können.

Auch die Ergebnisse von Native Query FYI, JPA 2.0 als Karte

0
Darshan Patel

Alter Stil mit 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

wenn Sie Spring verwenden, können Sie org.springframework.jdbc.core.RowMapper verwenden.

Hier ist ein Beispiel: 

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

Verwenden des Ruhezustands:

@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

Nicht sicher, ob das hier passt, aber ich hatte eine ähnliche Frage und fand folgende einfache Lösung/Beispiel für mich:

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

In meinem Fall musste ich SQL-Teile verwenden, die an anderer Stelle in Strings definiert wurden, sodass ich NamedNativeQuery nicht einfach verwenden konnte.

0
Andreas L.

Wir haben das Problem folgendermaßen gelöst:

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