JPA継承@EntityGraphには、サブクラスのオプションの関連付けが含まれます


12

次のドメインモデルを前提Answerとして、Valuesとそれぞれのサブ子を含むすべてのをロードし、それをに入れてAnswerDTOJSONに変換したいと思います。私は実用的な解決策を持っていますが、アドホックを使用して取り除く必要があるN + 1の問題に悩まされています@EntityGraph。すべての関連付けが構成されますLAZY

ここに画像の説明を入力してください

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

アドホック使用@EntityGraph上のRepository方法私は値が上にN + 1を防止するために、プリフェッチされることを保証することができるAnswer->Value関連付けを。私の結果は問題ありませんselectedが、MCValuesの関連付けの遅延読み込みのため、別のN + 1問題があります。

これを使う

@EntityGraph(attributePaths = {"value.selected"})

selectedもちろん、フィールドは一部のValueエンティティの一部にすぎないためです。

Unable to locate Attribute  with the the given name [selected] on this ManagedType [x.model.Value];

selected値がaの場合にのみ、関連付けをフェッチしようとするようJPAに指示するにはどうすればよいMCValueですか?のようなものが必要ですoptionalAttributePaths

回答:


8

EntityGraph関連属性がスーパークラスの一部であり、それによってすべてのサブクラスの一部である場合にのみ、を使用できます。それ以外の場合は、現在取得EntityGraphしてExceptionいるで常に失敗します。

N + 1選択の問題を回避する最良の方法は、クエリを2つのクエリに分割することです。

最初のクエリは、属性MCValueを使用しEntityGraphてマッピングされた関連付けを取得するためにを使用してエンティティを取得しselectedます。そのクエリの後、これらのエンティティはHibernateの1次レベルキャッシュ/永続コンテキストに格納されます。Hibernateは、2番目のクエリの結果を処理するときにそれらを使用します。

@Query("SELECT m FROM MCValue m") // add WHERE clause as needed ...
@EntityGraph(attributePaths = {"selected"})
public List<MCValue> findAll();

次に、2番目のクエリはAnswerエンティティをフェッチし、を使用しEntityGraphて関連するValueエンティティもフェッチします。Valueエンティティごとに、Hibernateは特定のサブクラスをインスタンス化し、1次キャッシュにそのクラスのオブジェクトと主キーの組み合わせがすでに含まれているかどうかを確認します。その場合、Hibernateはクエリによって返されたデータの代わりに1次レベルキャッシュのオブジェクトを使用します。

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

MCValue関連selectedするエンティティを持つすべてのエンティティをすでにフェッチしているためAnswer、初期化されたvalue関連付けを持つエンティティを取得します。また、関連付けにMCValueエンティティが含まれている場合、そのselected関連付けも初期化されます。


2つのクエリを使用することを考えました。1つは回答と値を取得するためのクエリで、2つ目はselectedを含む回答を取得するためのクエリですMCValue。これには追加のループが必要であり、データセット間のマッピングを管理する必要があるのが嫌いでした。私はこれのためにHibernateキャッシュを活用するあなたの考えが好きです。結果を格納するためにキャッシュに依存することは(一貫性の観点から)どの程度安全かについて詳しく説明できますか?これは、クエリがトランザクションで行われたときに機能しますか?私は見つけるのが難しく、散発的な遅延初期化エラーを恐れています。
スタック

1
同じトランザクション内で両方のクエリを実行する必要があります。あなたがそれを行い、永続化コンテキストをクリアしない限り、それは絶対に安全です。1次レベルのキャッシュには常にMCValueエンティティが含まれます。また、追加のループは必要ありません。にMCValue結合し、Answer現在のクエリと同じWHERE句を使用する1つのクエリですべてのエンティティをフェッチする必要があります。:私はまた、今日のライブストリームでこのについて話しましたyoutu.be/70B9znTmi00?t=238それは3時58分にスタートしたが、私は間に他のいくつかの質問をした...
Thorbenヤンセン

フォローありがとうございます。また、このソリューションではサブクラスごとに1つのクエリが必要であることも付け加えておきます。したがって、保守性は私たちにとって大丈夫ですが、このソリューションはすべてのケースに適しているとは限りません。
スタック

最後のコメントを少し修正する必要があります。もちろん、問題が発生するサブクラスごとのクエリのみが必要です。また、サブクラスの属性については、を使用してSINGLE_TABLE_INHERITANCEいるため、これは問題にならないようです。
スタック

7

Spring-Dataがそこで何をしているのかはわかりませんが、そのためには、通常TREAT、サブアソシエーションにアクセスできるように演算子を使用する必要がありますが、その演算子の実装にはかなりバグがあります。Hibernateは暗黙的なサブタイププロパティアクセスをサポートしますが、これはここで必要になるものですが、Spring-Dataはこれを適切に処理できないようです。エンティティモデルに対して任意の構造をマッピングできるJPAの上で動作するライブラリである Blaze-Persistence Entity-Viewsをご覧になることをお勧めします。タイプセーフな方法でDTOモデルをマッピングでき、継承構造もマッピングできます。ユースケースのエンティティビューは次のようになります。

@EntityView(Answer.class)
interface AnswerDTO {
  @IdMapping
  Long getId();
  ValueDTO getValue();
}
@EntityView(Value.class)
@EntityViewInheritance
interface ValueDTO {
  @IdMapping
  Long getId();
}
@EntityView(TextValue.class)
interface TextValueDTO extends ValueDTO {
  String getText();
}
@EntityView(RatingValue.class)
interface RatingValueDTO extends ValueDTO {
  int getRating();
}
@EntityView(MCValue.class)
interface TextValueDTO extends ValueDTO {
  @Mapping("selected.id")
  Set<Long> getOption();
}

Blaze-Persistenceによって提供される春のデータ統合により、このようなリポジトリを定義し、結果を直接使用できます

@Transactional(readOnly = true)
interface AnswerRepository extends Repository<Answer, Long> {
  List<AnswerDTO> findAll();
}

マッピングしたものだけを選択するHQLクエリを生成AnswerDTOします。これは次のようなものです。

SELECT
  a.id, 
  v.id,
  TYPE(v), 
  CASE WHEN TYPE(v) = TextValue THEN v.text END,
  CASE WHEN TYPE(v) = RatingValue THEN v.rating END,
  CASE WHEN TYPE(v) = MCValue THEN s.id END
FROM Answer a
LEFT JOIN a.value v
LEFT JOIN v.selected s

私がすでに見つけたライブラリへのヒントに感謝しますが、2つの主な理由でそれを使用しませんでした:1)プロジェクトの存続期間にわたってサポートされるlibに依存することはできません(あなたの会社のblazebitはかなり小さく、初めに)。2)単一のクエリを最適化するために、より複雑な技術スタックにコミットすることはしません。(私はあなたのlibがもっと多くのことをできることを知っていますが、私たちは一般的な技術スタックを好み、むしろJPAソリューションがない場合はカスタムクエリ/変換を実装するだけです)。
スタック

1
Blaze-Persistenceはオープンソースであり、Entity-Viewsは多かれ少なかれ標準であるJPQL / HQLの上に実装されています。それが実装する機能は安定しており、標準の上で動作するため、将来のバージョンのHibernateでも動作します。ユースケースが1つしかないため、何かを紹介したくないとのことですが、エンティティビューを使用できるのはそれだけではないと思います。エンティティビューを導入すると、通常、ボイラープレートコードの量が大幅に削減され、クエリのパフォーマンスも向上します。あなたを助けるツールを使いたくないなら、そうしてください。
クリスチャンベイコフ

少なくとも問題を理解できず、解決策を提供します。したがって、元の問題で現在何が起こっているのか、JPAがそれをどのように解決できるのかが答えで説明されていなくても、賞金を獲得できます。私の考えでは、これはJPAでサポートされていないだけで、機能のリクエストになるはずです。JPAのみを対象とした、より複雑な回答に対する別の賞金を提供します。
スタック

それは単にJPAでは不可能です。どのJPAプロバイダーでも完全にはサポートされておらず、EntityGraphアノテーションでもサポートされていないTREAT演算子が必要です。したがって、これをモデル化できる唯一の方法は、明示的な結合を使用する必要があるHibernateの暗黙的なサブタイププロパティ解決機能を使用することです。
クリスチャンベイコフ

1
あなたの答えでは、ビューの定義は次のようになりますinterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
スタック

0

私の最新のプロジェクトはGraphQL(私にとって初めて)を使用しており、N + 1クエリで大きな問題があり、必要なときにテーブルのみを結合するようにクエリを最適化しようとしました。私が発見したCosium /春-データ-JPAエンティティグラフはかけがえのありません。JpaRepositoryエンティティグラフでクエリに渡すメソッドを拡張および追加します。次に、実行時に動的エンティティグラフを作成して、必要なデータのみの左結合を追加できます。

データフローは次のようになります。

  1. GraphQLリクエストを受信する
  2. GraphQLリクエストを解析し、クエリ内のエンティティグラフノードのリストに変換する
  3. 検出されたノードからエンティティグラフを作成し、実行のためにリポジトリに渡す

エンティティグラフ(たとえば__typename、graphqlから)に無効なノードを含めないという問題を解決するために、エンティティグラフの生成を処理するユーティリティクラスを作成しました。呼び出し元のクラスは、グラフを生成するクラス名を渡します。これにより、ORMによって維持されるメタモデルに対してグラフの各ノードが検証されます。ノードがモデルにない場合は、グラフノードのリストから削除されます。(このチェックは再帰的で、各子もチェックする必要があります)

これを見つける前に、Spring JPA / Hibernateのドキュメントで推奨されているプロジェクションや他のすべての代替手段を試しましたが、問題をエレガントに、または少なくとも大量の追加コードで解決するようには見えませんでした


スーパータイプでは認識されない関連付けの読み込みの問題をどのように解決しますか?また、他の回答で述べたように、純粋なJPAソリューションがあるかどうかを知りたいのですが、libには、のselectedすべてのサブタイプで関連付けが利用できないという同じ問題があると思いますvalue
スタック

あなたがGraphQLに興味があるなら、我々はまた、graphql-Javaにブレイズ・永続エンティティ・ビューの統合を持っている:persistence.blazebit.com/documentation/1.5/entity-view/manual/...
クリスチャンBeikov

@ChristianBeikovありがとうございます。ただし、SQPRを使用して、モデル/メソッドからプログラムでスキーマを生成しています
アーバー

コードファーストのアプローチが好きなら、GraphQLの統合が気に入るはずです。実際に使用される列/式のみをフェッチし、結合などを自動的に減らします。
クリスチャンベイコフ

0

コメントの後に編集:

申し訳ありませんが、最初のラウンドでは問題を理解していません。問題は、findAll()を呼び出そうとしたときだけでなく、spring-dataの起動時に発生します。

したがって、完全な例をナビゲートして、私のgithubからプルすることができます:https : //github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

このプロジェクト内で問題を簡単に再現して修正できます。

事実上、Springデータと休止状態は、デフォルトでは「選択された」グラフを決定することができず、選択されたオプションを収集する方法を指定する必要があります。

したがって、最初に、クラスAnswerの NamedEntityGraphsを宣言する必要があります

ご覧のとおり、クラスAnswerの属性値にNamedEntityGraphが2つあります。

  • ロードする特定の関係のない最初のすべての

  • 特定のMultichoice値の2番目。これを削除すると、例外が再現されます。

次に、タイプLAZYでデータをフェッチする場合は、トランザクションコンテキスト answerRepository.findAll()にいる必要があります。

@Entity
@Table(name = "answer")
@NamedEntityGraphs({
    @NamedEntityGraph(
            name = "graph.Answer", 
            attributeNodes = @NamedAttributeNode(value = "value")
    ),
    @NamedEntityGraph(
            name = "graph.AnswerMultichoice",
            attributeNodes = @NamedAttributeNode(value = "value"),
            subgraphs = {
                    @NamedSubgraph(
                            name = "graph.AnswerMultichoice.selected",
                            attributeNodes = {
                                    @NamedAttributeNode("selected")
                            }
                    )
            }
    )
}
)
public class Answer
{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(updatable = false, nullable = false)
    private int id;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "value_id", referencedColumnName = "id")
    private Value value;
// ..
}

問題は、-の関連付けvalueを取得するのではなく、がの場合に関連付けをAnswer取得することです。回答には、それに関する情報は含まれていません。selectedvalueMCValue
スタック

@Stuck回答ありがとうございます。MCValueクラスを教えてください。ローカルで問題を再現するように努めます。
bdzzaid

あなたは関連付けを定義したので、あなたの例では、唯一の作品OneToManyとして、 FetchType.EAGERしかし、質問に記載されているように:すべての関連付けがされていますLAZY
スタック

@Stuck前回の更新以降に回答を更新しました。私の回答が問題の解決に役立ち、オプションの関係を含むエンティティグラフを読み込む方法を理解するのに役立つことを願っています。
bdzzaid

あなたの「解決策」は、この質問が関係している元のN + 1問題にまだ悩まされています。テストのさまざまなトランザクションに挿入メソッドと検索メソッドを置き、jpaがselectedすべての回答に対してDBクエリを発行して、それらを事前にロードするのではないことがわかります。
スタック
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.