web-dev-qa-db-ja.com

graphql-springを使用したLazyInitializationException

私は現在、RESTサーバーをGraphQLに(少なくとも部分的に)移行している最中です。ほとんどの作業は完了しましたが、解決できないと思われるこの問題に遭遇しました。FetchType.LAZYを使用したgraphqlクエリのOneToMany関係です。

私が使用しているもの: https://github.com/graphql-Java/graphql-spring-boot および https://github.com/graphql-Java/graphql-Java-tools 統合用。

次に例を示します。

エンティティ:

@Entity
class Show {
   private Long id;
   private String name;

   @OneToMany
   private List<Competition> competition;
}

@Entity
class Competition {
   private Long id;
   private String name;
}

スキーマ:

type Show {
    id: ID!
    name: String!
    competitions: [Competition]
}

type Competition {
    id: ID!
    name: String
}

extend type Query {
    shows : [Show]
}

リゾルバー:

@Component
public class ShowResolver implements GraphQLQueryResolver {
    @Autowired    
    private ShowRepository showRepository;

    public List<Show> getShows() {
        return ((List<Show>)showRepository.findAll());
    }
}

この(省略形の)クエリでエンドポイントにクエリを実行すると、次のようになります。

{
  shows {
    id
    name
    competitions {
      id
    }
  }
}

私は得る:

org.hibernate.LazyInitializationException:ロールのコレクションを遅延初期化できませんでした:Show.competitions、プロキシを初期化できませんでした-セッションがありません

これで、このエラーが発生する理由とその意味はわかりましたが、これを修正する必要があるかどうかはわかりません。 GraphQLの利点のいくつかを無効にするため、すべての関係を熱心にフェッチするように誘惑することはしたくありません。解決策を探す必要があるかもしれないアイデアはありますか?ありがとう!

12
puelo

私はそれを解決し、graphql-Java-toolsライブラリのドキュメントをもっと注意深く読むべきだったと思います。基本的なクエリを解決するGraphQLQueryResolverのほかに、Showclass用のGraphQLResolver<T>も必要でした。これは次のようになります。

@Component
public class ShowResolver implements GraphQLResolver<Show> {
    @Autowired
    private CompetitionRepository competitionRepository;

    public List<Competition> competitions(Show show) {
        return ((List<Competition>)competitionRepository.findByShowId(show.getId()));
    }
}

これは、ライブラリにShowclass内の複雑なオブジェクトを解決する方法を指示し、最初のクエリがCompetitionobjectsを含めるように要求した場合にのみ使用されます。明けましておめでとうございます!

編集31.07.2019:私はそれ以来以下の解決策から離れました。長時間実行されるトランザクションが良い考えになることはめったにありません。この場合、アプリケーションをスケーリングすると問題が発生する可能性があります。非同期の問題でクエリをバッチ処理するためのDataLoaderの実装を開始しました。 DataLoaderの非同期性と組み合わせた長時間実行トランザクションは、デッドロックにつながる可能性があります: https://github.com/graphql-Java-kickstart/graphql-Java-tools/issues/58#issuecomment-398761715 (詳細については上と下)。以下のソリューションは削除しません。小規模なアプリケーションやバッチクエリを必要としないアプリケーションの開始点としてはまだ良いかもしれませんが、そうするときはこのコメントを覚えておいてください。

EDIT:ここで要求されているのは、カスタム実行戦略を使用した別のソリューションです。 graphql-spring-boot-startergraphql-Java-toolsを使用しています。

私は最初に次のようにGraphQLConfigを定義します。

@Configuration
public class GraphQLConfig {
    @Bean
    public Map<String, ExecutionStrategy> executionStrategies() {
        Map<String, ExecutionStrategy> executionStrategyMap = new HashMap<>();
        executionStrategyMap.put("queryExecutionStrategy", new AsyncTransactionalExecutionStrategy());
        return executionStrategyMap;
    }
}

AsyncTransactionalExecutionStrategyは次のように定義されています:

@Service
public class AsyncTransactionalExecutionStrategy extends AsyncExecutionStrategy {

    @Override
    @Transactional
    public CompletableFuture<ExecutionResult> execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException {
        return super.execute(executionContext, parameters);
    }
}

これにより、クエリの実行全体が同じトランザクション内に配置されます。これが最適なソリューションであるかどうかはわかりません。また、エラー処理に関してすでにいくつかの欠点がありますが、そのように型リゾルバーを定義する必要はありません。

9
puelo

私の推奨する解決策は、サーブレットが応答を送信するまでトランザクションを開いておくことです。この小さなコード変更により、LazyLoadは正しく機能します。

import javax.servlet.Filter;
import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter;

@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

  /**
   * Register the {@link OpenEntityManagerInViewFilter} so that the
   * GraphQL-Servlet can handle lazy loads during execution.
   *
   * @return
   */
  @Bean
  public Filter OpenFilter() {
    return new OpenEntityManagerInViewFilter();
  }

}
5
Jaxt0r

受け入れられた回答について混乱している人は、Javaエンティティを変更して双方向の関係を含め、ヘルパーメソッドを使用してCompetitionを追加する必要があります。そうしないと忘れがちです。関係を正しく設定します。

_@Entity
class Show {
   private Long id;
   private String name;

   @OneToMany(cascade = CascadeType.ALL, mappedBy = "show")
   private List<Competition> competition;

   public void addCompetition(Competition c) {
      c.setShow(this);
      competition.add(c);
   }
}

@Entity
class Competition {
   private Long id;
   private String name;

   @ManyToOne(fetch = FetchType.LAZY)
   private Show show;
}
_

受け入れられた答えの背後にある一般的な直感は次のとおりです。

GraphqlリゾルバーShowResolverは、ショーのリストを取得するためにトランザクションを開きますが、それが完了するとトランザクションを閉じます。

次に、competitionsのネストされたgraphqlクエリは、前のクエリから取得した各ShowインスタンスでgetCompetition()を呼び出そうとします。これにより、トランザクションが原因でLazyInitializationExceptionがスローされます。もう閉店した。

_{
  shows {
    id
    name
    competitions {
      id
    }
  }
}
_

受け入れられた答えは、基本的にOneToMany関係を介して競合のリストを取得することをバイパスし、代わりに問題を排除する新しいトランザクションで新しいクエリを作成します。

これがハックかどうかはわかりませんが、リゾルバーの_@Transactional_は機能しませんが、そのロジックはある程度意味がありますが、根本的な原因を明確に理解していません。

2
reversebind