コンストラクタ式でn + 1の選択を行うのを防ぐ


動機づけ


JPA/Hibernateと基本的なcrudの操作は簡単ですが、すべてのアプリケーションは遅かれ早かれ、予測のような特定のユースケースのためのDTOスタイルの結果オブジェクトを導入する必要があります.それは取引の一部です、またはマッパーはあなたのために宿題を行うことができます.
幸いにも、JPAはクエリから期待する結果オブジェクトを正確に指定する方法を与えてくれます.そのための一つの方法は、クエリーのSELECT部分にJPQLを使用することです.
select new com.example.NameDto(e.firstName, e.lastName) from Employee e
注意new キーワードと完全修飾クラス名.これは、あなたがそれがすると思うものを正確にします.新しいインスタンスを作成しますNameDto 各結果行の適切なパブリックコンストラクターを使用します.

問題


エンティティの一部を使用してDTOに投影するのは良いですが、エンティティ全体をフェッチしてDTOにラップしたい場合はどうなりますか?エンティティを選択するユースケースと、エンティティと一緒に格納されていない補助情報も選択します.たとえば、私たちは“2000 mの半径で私の近くの場所”を選択したいと仮定します.それは条件の1つだけであり、その後、実際の場所のエンティティの結果セットを右ですか?我々はまた、エンドユーザーにどのように彼の位置から離れてどのように場所が表示されますが表示されますか?なぜ、我々はちょうど包みますPlace また、距離を保持する適切なDTODistanceResult ? それを試してみましょう.
ここに我々はPlace エンティティは、場所がどこにあるかを示す場所フィールドを持つことがわかります.

それから私たちはDistanceResult これは、クエリの原点からどれだけ離れているかとともに、エンティティをラップするだけです.

これにより、以下のような単純なjpqlクエリを実行しましょう.
select new com.example.DistanceResult(p, distance(:center, p.location))
  from Place p
  where dwithin(:center, p.location, :radiusMeters) = true
ここでは地理的な型や関数を扱うことを一時的に無視してください.これは実際にはhibernate-spatial そして、私はpostgis しかし、これは問題ではありません.
これは問い合わせログがその問い合わせに与えるものです.
select place1_.id as col_0_0_, st_distance(?, place0_.location) as col_1_0_ from places place0_ inner join places place1_ on (place0_.id=place1_.id) where st_dwithin(?, place0_.location, ?)=true

select place0_.id as id1_0_0_, place0_.location as location2_0_0_, place0_.name as name3_0_0_ from places place0_ where place0_.id=?

select place0_.id as id1_0_0_, place0_.location as location2_0_0_, place0_.name as name3_0_0_ from places place0_ where place0_.id=?
私たちの特異なJPQLクエリーが結果として大体そのSQL等価である1つの質問をもたらしたのをはっきりと見ることができます.その上に、私たちの結果において、それぞれのエンティティインスタンスに対して追加の選択を見ることができます
セット(ここで結果サイズは2です).これは恐ろしいN + 1問題に似ています.

結果1:結果変圧器


Hiberanteプロジェクトの片手では、コンストラクタ式を使用する理由は、完全なエンティティを取得するのではなく、すぐに必要な部分だけを取得する理由について明確に推論していました.一方、指定されたユースケースでは、N + 1は我々が持ちたいものは何もありません.悲しいことに、全体のエンティティを取得するHibernateを伝える明確な方法はありません.例えば、fetch キーワードは動作しません.またfetch all properties この場合、Stanzaは結果のSQLに影響しません.
幸運にも、我々が持っていたいDTO具体化をする若干の他の方法があります.これはHibernate特有で、呼ばれますResultTransformer . 任意のHibernateクエリを指定すると、このインターフェイスのインスタンスをアタッチでき、結果を処理できます.

春のデータJPAと結果のトランスフォーマー


私はJPAの大部分を信頼します、そして/または、Hibernateユーザーは多分ボイラー板を避けるためにスプリングデータJPAを使用するでしょう.使い方はこちらResultTransformer その場合:
まず何か空想なしで通常のリポジトリインターフェースを持っています.
@Repository
public interface PlaceRepository extends PagingAndSortingRepository<Place, Long>,
    DistancePlaceRepository {
}
これは、追加のインターフェイスですが、我々の距離クエリメソッドがここで定義されて見ることができます.
public interface DistancePlaceRepository {
    List<DistanceResult> findInDistance(Point center, double radiusMeters);
}
しかし、この方法はどのように実装されていますか?我々は別のを追加することができます@Repository これを実装するコンテキストにBean
インターフェイス.Spring Data JPAは、実際には以下の実装を使用しています.
@Repository
public class DistancePlaceRepositoryImpl implements DistancePlaceRepository {

    @PersistenceContext
    private EntityManager entityManager;

    @SuppressWarnings({"unchecked", "deprecation"})
    @Override
    public List<DistanceResult> findInDistance(Point center, double radiusMeters) {
        String jpql = "select p, distance(:center, p.location) from Place p where dwithin(:center, p.location, :radiusMeters) = true";

        return (List<DistanceResult>) entityManager.createQuery(jpql)
                .setParameter("center", center)
                .setParameter("radiusMeters", radiusMeters)
                .unwrap(Query.class)
                .setResultTransformer(
                        (ListResultTransformer)
                                (tuple, aliases) -> new DistanceResult(
                                        (Place) tuple[0],
                                        ((Number) tuple[1]).doubleValue()
                                )
                ).getResultList();
    }
}

I'm using Vlad Mihalcea's hibernate-types library here to get access to ListResultTransformer (See links below).


では、このトランスを使用する場合、どのようにクエリログがどのように見えますか?
select place0_.id as col_0_0_, st_distance(?, place0_.location) as col_1_0_, place0_.id as id1_0_, place0_.location as location2_0_, place0_.name as name3_0_ from places place0_ where st_dwithin(?, place0_.location, ?)=true
としては、はるかに優れている唯一の単一の選択を見ることができます.n + 1は見えない.

欠点


あなたが使用しようとするときに注意してください最初のものResultTransformer を使用する実際のメソッドですかQuery#setResultTransformer としてマークされます.これは実際に、Hibernate開発者による早めの非難の何らかの形です.Hibernate 6.0を使用していない場合は、この推奨されないメソッドを使用する方法はありません.したがって、ここでの警告を無視するのは明らかです.
注意すべき第二のものは明らかにこの解決策が休止的であることである.ResultTransformer JPAは(まだ)同等ではありません.

ソリューション2 :ブレイズ持続性エンティティビュー


インターネット上でこの問題と関連情報を調査している間、私はスタックオーバーフローの上で起こりましたanswer いわゆる「炎持続性実体見解」を発見するために私を導いてください.これで私たちのDTOを再定義できそうです.私はそれを介して、読者にはdocumentation どのように正確に我々は全体のエンティティのラッピングを達成し、また、私たちの元のクエリの距離部分をマップします.その豊富なライブラリ、私は確実に方法があることを確認します.

ソリューション3 : jooqの使用


最後に、しかし、少なくとも、私はまだ言及しなければなりません、もちろん、ちょうど我々の既存のHibernateマッピングを少しだけ混ぜているのを防ぐことは、何もありませんjOOQ 側に.これはSQLが正確に実行しているSQLを完全に制御できます.そして、我々は簡単に我々が望むならば、結果として生じるタプルを我々のDTOに包むことができます.確かに我々は結果が管理された実体でないと注意しなければなりません、しかし、我々のユースケースでは、これは最初に必要でありません.
私は以前に空間関数に注意を払わないと言いました、しかし、我々が実際にこの種の質問のためにJooqを使いたいならば、我々はクエリービルダーでそれらのSQL機能のサポートを必要とするでしょう.幸いにも、Githubにこのプロジェクトの名前がありますjooq-postgis-spatial ( PostGISの場合)

リンク

  • Hibernate結果変圧器のVLAD mihalcea :Blog Post
  • 持続性エンティティdocumentation
  • オリジナルの質問stackoverflow
  • JOOQhomepage
  • 写真でJason Leung on Unsplash