JPA と Hibernate を使用して射影クエリを DTO (データ転送オブジェクト) にマップする最良の方法
はじめに
この記事では、JPA と Hibernate を使用する場合に DTO プロジェクションをマップする最良の方法を紹介します。
Hibernate フォーラムで質問に答えているときに、次の質問に出くわしました。同じトピックに関する以前の質問と異なっていたのは、元の投稿者が、Hibernate によってエレガントに対処できる JPA コンストラクター式の欠点を認識していたことです。
これは StackOverflow と Hibernate フォーラムで繰り返されるテーマであるため、JPA と Hibernate を使用して DTO プロジェクションをマッピングする最良の方法に特化した記事を作成することにしました。
ドメイン モデル
次の Post
があるとします。 エンティティ:
前に説明したように、エンティティのフェッチは、エンティティを変更する予定がある場合にのみ意味があります。 DTO プロジェクションのみに関心がある場合は、呼び出し元が本当に必要とする列のみを選択する方が効率的です。
id
だけを選択したいとします。 そして title
Post
から Post
全体を選択するのはリソースの無駄です。 JPA と Hibernate を使用してこの目標を達成する方法を見てみましょう。
JPA を使用した DTO プロジェクション
JPA または Hibernate を使用する場合、JPQL または Criteria API またはネイティブ SQL クエリを介して両方のエンティティ クエリを実行できます。
JPA タプルと JPQL を使用した DTO プロジェクション
プロジェクションに DTO クラスを提供したくない場合は、JPA Tuple
を使用できます .
したがって、 Tuple
を使用するには プロジェクションの場合、JPQL クエリは次のようになります。
List<Tuple> postDTOs = entityManager.createQuery(""" select p.id as id, p.title as title from Post p where p.createdOn > :fromTimestamp """, Tuple.class) .setParameter( "fromTimestamp", Timestamp.from( LocalDate.of(2020, 1, 1) .atStartOfDay() .toInstant(ZoneOffset.UTC) ) ) .getResultList(); assertFalse(postDTOs.isEmpty()); Tuple postDTO = postDTOs.get(0); assertEquals( 1L, postDTO.get("id") ); assertEquals( "High-Performance Java Persistence", postDTO.get("title") );
ご覧のとおり、Tuple
サポートが必要なプロジェクションのタイプごとに DTO クラスを指定する必要がないため、DTO プロジェクションをフェッチする便利な方法です。
コンストラクタ式と JPQL を使用した DTO プロジェクション
Tuple
を使用したくない場合 DTO プロジェクションで特定のクラスを使用する必要があるため、コンストラクタ式を使用できます NEW
を指定して キーワードと、DTO プロジェクションを表すクラスの完全修飾名、およびコンストラクター引数として渡される属性のリスト。
DTO クラスは、結果セット プロジェクションによってフェッチされたすべての属性を取得するコンストラクターを提供する必要があります。
したがって、DTO プロジェクションは次のようになります。
public class PostDTO { private Long id; private String title; public PostDTO(Number id, String title) { this.id = id.longValue(); this.title = title; } public Long getId() { return id; } public String getTitle() { return title; } }
したがって、コンストラクター式の JPQL クエリは次のようになります。
List<PostDTO> postDTOs = entityManager.createQuery(""" select new com.vladmihalcea.book.hpjp.hibernate.forum.dto.PostDTO( p.id, p.title ) from Post p where p.createdOn > :fromTimestamp """, PostDTO.class) .setParameter( "fromTimestamp", Timestamp.from( LocalDate.of(2020, 1, 1) .atStartOfDay() .toInstant(ZoneOffset.UTC) ) ) .getResultList();
この記事に記載されている手順に従っている場合は、構築式のパッケージ名を省略できます。
タプルとネイティブ SQL クエリを使用した DTO プロジェクション
Hibernate ORM 5.2.11 から、HHH-11897 Jira の問題が修正されたため、Tuple
を使用できます。 ネイティブ SQL クエリ用。
List<Tuple> postDTOs = entityManager.createNativeQuery(""" SELECT p.id AS id, p.title AS title FROM Post p WHERE p.created_on > :fromTimestamp """, Tuple.class) .setParameter( "fromTimestamp", Timestamp.from( LocalDate.of(2020, 1, 1) .atStartOfDay() .toInstant(ZoneOffset.UTC) ) ) .getResultList(); assertFalse(postDTOs.isEmpty()); Tuple postDTO = postDTOs.get(0); assertEquals( 1L, postDTO.get("id") ); assertEquals( "High-Performance Java Persistence", postDTO.get("title") );
ConstructorResult を使用した DTO プロジェクション
ネイティブ SQL クエリの場合、コンストラクタ式を使用できなくなったため、名前付きネイティブ クエリを使用して、指定された SqlResultSetMapping
を構成する必要があります。 コンストラクターまたはフィールドを介して DTO クラスを設定できるようにします。
同じ PostDTO
を使用する場合 前に紹介したクラス タイプでは、次の SqlResultSetMapping
を提供する必要があります。 :
@NamedNativeQuery( name = "PostDTO", query = """ SELECT p.id AS id, p.title AS title FROM Post p WHERE p.created_on > :fromTimestamp """, resultSetMapping = "PostDTO" ) @SqlResultSetMapping( name = "PostDTO", classes = @ConstructorResult( targetClass = PostDTO.class, columns = { @ColumnResult(name = "id"), @ColumnResult(name = "title") } ) )
ここで、native query という名前の SQL プロジェクションが次のように実行されます:
List<PostDTO> postDTOs = entityManager .createNamedQuery("PostDTO") .setParameter( "fromTimestamp", Timestamp.from( LocalDateTime.of(2020, 1, 1, 0, 0, 0) .toInstant(ZoneOffset.UTC) ) ) .getResultList();
JPA SqlResultSetMapping
を使用する最良の方法の詳細については、 注釈については、この記事をお読みください。
Hibernate を使用した DTO プロジェクション
JPA のすべての機能を Hibernate で使用できますが、標準の Java Persistence 仕様よりも多くの機能を Hibernate が提供する必要があります。
ResultTransformer と JPQL を使用した DTO プロジェクション
前に説明したように、ResultTransformer
結果セットを好きなようにカスタマイズできるので、それを使用して典型的な Object[]
を変換できます DTO 結果セットへの配列射影。
今回は、クエリによって選択されるエンティティ属性に一致するコンストラクターを提供する必要はありません。
DTO クラスでセッターを提供する必要さえありませんが、ここでは BigInteger
であるため、セッターが必要です。 id
に対して返される可能性があります Long
としてキャストする必要があるデータベース列 .
Hibernate は Reflection を使用して適切なフィールドを設定できるため、以前の JPA コンストラクター式の代替手段よりも柔軟です。
次の DTO クラスがあるとします:
public class PostDTO { private Long id; private String title; public Long getId() { return id; } public void setId(Number id) { this.id = id.longValue(); } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } }
setResultTransformer
を使用して結果セットを変換できます Hibernate 固有の org.hibernate.query.Query
のメソッド JPA Query
からアンラップできるインターフェース .
List<PostDTO> postDTOs = entityManager.createQuery(""" select p.id as id, p.title as title from Post p where p.createdOn > :fromTimestamp """) .setParameter( "fromTimestamp", Timestamp.from( LocalDateTime.of(2020, 1, 1, 0, 0, 0) .toInstant(ZoneOffset.UTC) ) ) .unwrap(org.hibernate.query.Query.class) .setResultTransformer(Transformers.aliasToBean(PostDTO.class)) .getResultList();
ResultTransformer とネイティブ SQL クエリを使用した DTO プロジェクション
ネイティブの SQL クエリを使用する場合は、SqlResultSetMapping
を宣言する手間を省くことができます。 AliasToBeanResultTransformer
を使用できるため 前述の JPQL の例と同様です。
List<PostDTO> postDTOs = entityManager.createNativeQuery(""" select p.id as "id", p.title as "title" from Post p where p.created_on > :fromTimestamp """) .setParameter( "fromTimestamp", Timestamp.from( LocalDateTime.of(2020, 1, 1, 0, 0, 0) .toInstant(ZoneOffset.UTC) ) ) .unwrap(org.hibernate.query.NativeQuery.class) .setResultTransformer(Transformers.aliasToBean(PostDTO.class)) .getResultList();
いいですね。
結論
ご覧のとおり、JPA と Hibernate を使用して DTO プロジェクションを生成するための複数のオプションがあります。 JPA 仕様はコンストラクター式と Tuple
の両方を提供していますが、 結果、ResultTransformer
はるかに柔軟な代替手段となる可能性があります。