web-dev-qa-db-ja.com

JPA:ネイティブクエリ結果セットをPOJOクラスコレクションに変換する方法

私は自分のプロジェクトでJPAを使用しています。

私は5つのテーブルで結合操作を行う必要があるクエリに来ました。だから私は5つのフィールドを返すネイティブクエリを作成しました。

今度は結果オブジェクトを同じ5つのストリングを含むJava POJOクラスに変換したいと思います。

JPAに直接その結果をPOJOオブジェクトリストにキャストする方法はありますか。

私は次の解決策に至りました。

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

これでresultClassに、実際のJPAエンティティであるクラスを提供する必要がありますか? OR同じ列名を含む任意のJava POJOクラスに変換できますか?

146
Gunjan Shah

JPAは、 SqlResultSetMapping を提供しています。これにより、ネイティブクエリから返されたものをエンティティにマッピングできます。 またはカスタムクラス

EDITJPA 1.0では、エンティティ以外のクラスへのマッピングは許可されていません。 JPA 2.1でのみ、戻り値をJavaクラスにマッピングするための ConstructorResult が追加されました。

また、カウントの取得に関するOPの問題については、単一の ColumnResult を使用して結果セットマッピングを定義するだけで十分なはずです。

88
Denis Tulskiy

これに対する解決策をいくつか見つけました。

マッピングされたエンティティの使用(JPA 2.0)

JPA 2.0を使用すると、ネイティブクエリをPOJOにマッピングすることはできません。エンティティでのみ行うことができます。

例えば:

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

ただし、この場合、Jediはマップされたエンティティクラスである必要があります。

ここで未チェックの警告を回避する別の方法は、名前付きのネイティブクエリを使用することです。したがって、エンティティでネイティブクエリを宣言すると

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

それから、私たちは簡単にできる:

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

これはより安全ですが、マップされたエンティティの使用に制限されています。

手動マッピング

私が少し実験したソリューション(JPA 2.1が登場する前)は、少しのリフレクションを使用してPOJOコンストラクターに対してマッピングを行っていました。

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

このメソッドは基本的に(ネイティブクエリによって返される)Tuple配列を受け取り、同じ数のフィールドと同じタイプを持つコンストラクターを検索することにより、提供されたPOJOクラスに対してマッピングします。

次に、次のような便利な方法を使用できます。

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

そして、次のようにこの手法を簡単に使用できます。

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

@ SqlResultSetMappingを使用したJPA 2.1

JPA 2.1の登場により、@ SqlResultSetMappingアノテーションを使用して問題を解決できます。

エンティティ内のどこかに結果セットのマッピングを宣言する必要があります。

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

そして、次のことを行います。

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

もちろん、この場合、Jediはマップされたエンティティである必要はありません。通常のPOJOでもかまいません。

XMLマッピングを使用

私はこれらすべての@SqlResultSetMappingをエンティティに追加することに非常に不満を感じる人の1人であり、エンティティ内の名前付きクエリの定義が特に嫌いなので、代わりに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>

そして、これらはすべて私が知っている解決策です。最後の2つは、JPA 2.1を使用できる場合の理想的な方法です。

183
Edwin Dalorzo

はい、JPA 2.1では簡単です。非常に便利な注釈があります。彼らはあなたの人生を簡素化します。

最初にネイティブクエリを宣言し、次に結果セットマッピング(データベースから返されたデータのPOJOへのマッピングを定義します)を宣言します。参照するPOJOクラスを書いてください(簡潔にするためにここには含まれていません)。大事なことを言い忘れましたが、DAOでメソッドを作成して(たとえば)クエリを呼び出します。これは私にとってdropwizard(1.0.0)アプリでうまくいきました。

まず、エンティティクラスでネイティブクエリを宣言します。

@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

その下に、結果セットマッピング宣言を追加できます。

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

DAOの後半では、クエリを次のように参照できます。

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

それでおしまい。

10
John

あなたがSpring-jpaを使うならば、これは答えとこの質問に対する補足です。何か欠陥があればこれを修正してください。私が満たすべき実用的な必要性に基づいて、主に3つの方法を使用して「結果Object[]をpojoにマッピング」しました。

  1. JPA内蔵方式で十分です。
  2. JPAの組み込みメソッドでは不十分ですが、カスタマイズされたsqlとそのEntityで十分です。
  3. 前の2つは失敗しました、そして私はnativeQueryを使わなければなりません。これがその例です。予想されるプジョー:

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

方法1:pojoをインターフェースに変更します。

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

そしてリポジトリ:

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

方法2:リポジトリ:

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

注:POJOコンストラクタのパラメータシーケンスは、POJO定義とSQLの両方で同一でなければなりません。

方法3:Edwin Dalorzoの回答の例として、Entity@SqlResultSetMapping@NamedNativeQueryを使用します。

最初の2つのメソッドは、カスタマイズされたコンバーターのように、多くの中間ハンドラーを呼び出します。例えば、AntiStealingsecretKeyを定義し、永続化される前にそれを暗号化するためにコンバーターが挿入されます。これは最初の2つのメソッドがsecretKeyに変換して返すことになりますが、これは私が望んでいないことです。方法3はコンバーターを克服し、返されるsecretKeyは保存されているものと同じです(暗号化されたもの)。

7
Tiina

まず以下のアノテーションを宣言します。

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

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

次に、POJOに次のように注釈を付けます。

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

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

それから注釈プロセッサを書いてください:

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

次のように上記のフレームワークを使用してください。

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

結果を非エンティティ(Beans/POJO)に割り当てるために、展開手順を実行できます。手順は以下の通りです。

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

使い方はJPA-Hibernate実装用です。

3
zawhtut

Hibernateでは、このコードを使用してネイティブクエリを簡単にマッピングできます。

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

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

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

これがここに収まるかどうかわからないが、私は同様の質問をし、私のための次の簡単な解決策/例を見つけた:

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

私の場合は、他の場所で文字列で定義されたSQL部分を使用しなければならなかったので、私は単にNamedNativeQueryを使用することはできませんでした。

1
Andreas L.

必要なのはコンストラクタを持つDTOだけです。

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

そしてそれを呼び出します。

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

最も簡単な方法は、so射影を使用することです。クエリ結果を直接インターフェイスにマッピングでき、SqlResultSetMappingを使用するよりも実装が簡単です。

例を以下に示します。

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

}

射影インタフェースのフィールドは、このエンティティのフィールドと一致する必要があります。そうでなければフィールドマッピングは壊れるかもしれません。

また、SELECT table.column表記を使用する場合は、例に示すようにエンティティからの名前に一致するエイリアスを常に定義してください。

0
Thanthu

他の人がすでにすべての可能な解決策を述べているので、私は私の回避策の解決策を共有しています。

私のPostgres 9.4の状況では、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>>() {});

私はあなたが他のデータベースについても同じことができると確信しています。

またFYI、 JPA 2.0ネイティブクエリの結果をマップとして

0
Darshan Patel

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

springを使っているなら、org.springframework.jdbc.core.RowMapperを使うことができます。

これが一例です。

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

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

次の方法で問題を解決しました:

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