Java >> Java チュートリアル >  >> Tag >> hibernate

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 はるかに柔軟な代替手段となる可能性があります。


Java タグ