Je suis actuellement en train de migrer mon serveur REST vers GraphQL (au moins en partie). La plupart du travail est terminé, mais je suis tombé sur ce problème que je ne semble pas pouvoir résoudre: les relations OneToMany dans une requête graphql, avec FetchType.LAZY.

J'utilise: https://github.com/graphql-java/graphql-spring-boot et https://github.com/graphql-java/graphql-java-tools pour l'intégration.

Voici un exemple:

Entités:

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

   @OneToMany(mappedBy = "show")
   private List<Competition> competition;
}

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

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

Schéma:

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

type Competition {
    id: ID!
    name: String
}

extend type Query {
    shows : [Show]
}

Résolveur:

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

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

Si j'interroge maintenant le point de terminaison avec cette requête (abrégée):

{
  shows {
    id
    name
    competitions {
      id
    }
  }
}

Je reçois:

org.hibernate.LazyInitializationException: échec de l'initialisation paresseuse d'une collection de rôles: Show.competitions, impossible d'initialiser le proxy - pas de session

Maintenant, je sais pourquoi cette erreur se produit et ce que cela signifie, mais je ne sais pas vraiment s'il fallait appliquer un correctif pour cela. Je ne veux pas obliger mes entités à récupérer toutes les relations avec empressement, car cela annulerait certains des avantages de GraphQL. Des idées pour lesquelles je pourrais avoir besoin de chercher une solution? Merci!

13
puelo 30 déc. 2017 à 23:25

6 réponses

Meilleure réponse

Je l'ai résolu et j'aurais dû lire plus attentivement la documentation de la bibliothèque graphql-java-tools, je suppose. En plus de GraphQLQueryResolver qui résout les requêtes de base, j'avais également besoin d'un GraphQLResolver<T> pour ma classe Show, qui ressemble à ceci:

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

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

Cela indique à la bibliothèque comment résoudre des objets complexes dans ma classe Show et n'est utilisé que si la requête initiale demande d'inclure les objets Competition. Bonne année!

EDIT 31.07.2019 : depuis, je me suis éloigné de la solution ci-dessous. Les transactions de longue durée sont rarement une bonne idée et dans ce cas, elles peuvent causer des problèmes une fois que vous faites évoluer votre application. Nous avons commencé à implémenter des DataLoaders pour les requêtes par lots dans une affaire asynchrone. Les transactions de longue durée associées à la nature asynchrone des DataLoaders peuvent entraîner des blocages: https://github.com/graphql-java-kickstart/graphql-java-tools/issues/58#issuecomment-398761715 (ci-dessus et ci-dessous pour plus d'informations). Je ne supprimerai pas la solution ci-dessous, car cela pourrait toujours être un bon point de départ pour les applications plus petites et / ou les applications qui n'auront pas besoin de requêtes groupées, mais gardez ce commentaire à l'esprit lorsque vous le faites.

MODIFIER: Comme demandé, voici une autre solution utilisant une stratégie d'exécution personnalisée. J'utilise graphql-spring-boot-starter et graphql-java-tools:

Je définis d'abord une configuration GraphQL comme ceci:

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

AsyncTransactionalExecutionStrategy est défini comme ceci:

@Service
public class AsyncTransactionalExecutionStrategy extends AsyncExecutionStrategy {

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

Cela place toute l'exécution de la requête dans la même transaction. Je ne sais pas si c'est la solution la plus optimale, et elle présente également déjà quelques inconvénients en ce qui concerne la gestion des erreurs, mais vous n'avez pas besoin de définir un résolveur de type de cette façon.

10
puelo 31 juil. 2019 à 15:18

Je suppose que chaque fois que vous récupérez un objet de Show , vous voulez tout le Competition associé de l'objet Show .

Par défaut, le type de récupération pour tous les types de collections dans une entité est LAZY . Vous pouvez spécifier le type EAGER pour vous assurer que hibernate récupère la collection.

Dans votre classe Show , vous pouvez changer le fetchType en EAGER .

@OneToMany(cascade=CascadeType.ALL,fetch=FetchType.EAGER)
private List<Competition> competition;
-3
Karol Selak 31 déc. 2017 à 09:09

Pour moi, l'utilisation de AsyncTransactionalExecutionStrategy ne fonctionnait pas correctement avec des exceptions. Par exemple. Lazy init ou une exception au niveau de l'application a déclenché la transaction à l'état de restauration uniquement. Le mécanisme de transaction Spring a ensuite lancé une transaction d'annulation uniquement à la limite de la stratégie execute, ce qui a obligé HttpRequestHandlerImpl à renvoyer 400 réponses vides. Voir https://github.com/graphql-java-kickstart / graphql-java-servlet / issues / 250 et https: / /github.com/graphql-java/graphql-java/issues/1652 pour plus de détails.

Ce qui a fonctionné pour moi, c'est d'utiliser Instrumentation pour encapsuler toute l'opération dans une transaction: https://spectrum.chat/graphql/general/transactional-queries-with-spring~47749680-3bb7-4508-8935-1d20d04d0c6a

0
Oleg Efimov 21 mai 2020 à 06:00

Ma solution préférée est d'ouvrir la transaction jusqu'à ce que le servlet envoie sa réponse. Avec ce petit changement de code, votre Lazy Load fonctionnera correctement:

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();
  }

}
8
Jaxt0r 24 août 2018 à 07:20

Il vous suffit d'annoter vos classes de résolveur avec @Transactional. Ensuite, les entités renvoyées par les référentiels pourront récupérer les données paresseusement.

-3
Yogu 17 mai 2018 à 15:37

Pour toute personne confuse au sujet de la réponse acceptée, vous devez modifier les entités Java pour inclure une relation bidirectionnelle et vous assurer d'utiliser les méthodes d'assistance pour ajouter un Competition, sinon il est facile d'oublier de définir la relation correctement.

@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;
}

L'intuition générale derrière la réponse acceptée est:

Le résolveur graphql ShowResolver ouvrira une transaction pour obtenir la liste des émissions mais ensuite il fermera la transaction une fois qu'il aura terminé.

Ensuite, la requête graphql imbriquée pour competitions tentera d'appeler getCompetition() sur chaque instance Show récupérée de la requête précédente qui lancera un LazyInitializationException car la transaction a été fermée.

{
  shows {
    id
    name
    competitions {
      id
    }
  }
}

La réponse acceptée est essentiellement contourner la récupération de la liste des compétitions via la relation OneToMany et créer à la place une nouvelle requête dans une nouvelle transaction qui élimine le problème.

Je ne sais pas s'il s'agit d'un hack, mais @Transactional sur les résolveurs ne fonctionne pas pour moi bien que la logique de le faire ait un sens, mais je ne comprends clairement pas la cause profonde.

2
reversebind 1 juil. 2018 à 10:36