Kann ich einen GraphQLQueryResolver sicher von einem GraphQLResolver zurückrufen?

Sep 24 2020

Meine Frage ist: Was ist das Beste bei der Implementierung von Auflösungsmethoden ? direkter Aufruf von Datenrepositorys oder Rückruf eines Hauptresolvers oder einer Implementierung GraphQLQueryResolver(vorausgesetzt, er verfügt über geeignete Methoden)? Mit anderen Worten (siehe Beispiel unten): DataFetchingEnvironmentWird der Wert beim Zurückrufen eines Hauptauflösers richtig eingestellt / eingestellt ?

Hinweis: Wenn Sie nicht mit der ResolversArbeit mit GraphQL Java Tools vertraut sind , lassen Sie sich einfach einen Blick auf @ werfenhttps://www.graphql-java-kickstart.com/tools/schema-definition/

Nun das Beispiel.

In einer Spring Boot-App mit GraphQL Java Tools (mit der graphql-spring-boot-starterAbhängigkeit) haben wir dieses Schema:

type User {
  id: ID
  name: String
  company: Company
}

type Company {
  id: ID
  name: String
}

mit passendem POJO oder Entität (Getter / Setter werden weggelassen):

class User {

  private Long id;
  private String name;
  private Long idCompany;

}

class Company {

  private Long id;
  private String name;

}

und diese Resolver (Hinweis: UserRepository und CompanyRepository sind Ihre üblichen DAO / Repository-Klassen, die entweder von Spring Data (JPA), etwas anderem oder Ihrer eigenen benutzerdefinierten Implementierung unterstützt werden, was auch immer ...):

QueryResolver implements GraphQLQueryResolver {

  @Autowired
  private UserRepository userRepository;

  @Autowired
  private CompanyRepository companyRepository;

  public User user(String id) {
    return userRepository.findById(id);
  }

  public Company company(String idCompany) {
    return companyRepository.findById(idCompany);
  }

}

UserResolver implements GraphQLResolver<User> {

  @Autowired
  private CompanyRepository companyRepository;

  public Company company(User user) {
    return companyRepository.findById(user.getIdCompany());
  }

  // ...or should I do:

  @Autowired
  private QueryResolver queryResolver;

  public Company company(User user) {
    return queryResolver.company(user.getIdCompany());
  }

}

Dies ist (sinnvoller), wenn Sie DataFetchingEnvironment environmentam Ende jeder Methode hinzufügen UND sie verwenden, bevor Sie die Aufrufe an die verschiedenen (Daten-) Repositorys ausführen.

Wenn Sie mit dem obigen Beispiel DataFetchingEnvironmentfortfahren , wäre es richtig, dies zu tun (dh wäre das ordnungsgemäß ausgefüllt, wenn es erneut an den Haupt-QueryResolver übertragen wird)?

UserResolver implements GraphQLResolver<User> {

  @Autowired
  private QueryResolver queryResolver;

  public Company company(User user, DataFetchingEnvironment environment) {
    return queryResolver.company(user.getIdCompany(), environment);
  }

}

Antworten

1 AllirionX Sep 25 2020 at 04:44

Kurze Antwort

Sie können Ihre Resolver-Aufrufe an die Service-Schicht delegieren, die DataFecthingEnvironment jedoch nicht zwischen Resolvern / Services übergeben. Es wäre nicht richtig ausgefüllt.

Lange Antwort

Es ist nicht sicher und kann zu schwer zu lokalisierenden Fehlern und Datenverlusten führen.

Die DataFetchingEnvironment wird aus der ausgeführten graphql-Abfrage / Mutation ausgefüllt, und Sie würden erwarten, dass die DataFetchingEnvironment in Ihrer Resolver-Methode mit der aufgerufenen Resolver-Methode übereinstimmt.

Betrachten Sie das folgende Schema:

type Movie {
  id: ID!
  title: String!
  rating: String
  actors: [Actor]
}

type Actor {
  id: ID!
  name: String!
  role: String
}

input ActorUpdateInput {
  id: ID!
  name: String
  role: String
}

type Query {
  #Search movies with a specified Rating
  searchMovie(name: movieTitle, rating: String): Book
  #Search R-rated movies
  searchRRatedMovie(name: movieTitle): Book
}

type Mutation {
  #Update a movie and its actors
  updateMovie(id:Id!, title: String, actors: [ActorUpdateInput]): Movie
  #Update an actor
  updateActor(input: ActorUpdateInput!): Actor
}

Beispiel 1: Abfrage

query {
  searchRRatedMovie(name: "NotRRatedMovie") {
    title
  }
}

Der Film "NotRRatedMovie" hat kein R-Rating. Wir können davon ausgehen, dass diese Abfrage Nulldaten zurückgibt.

Die folgende Implementierung übergibt nun die DataFetchingEnvironment von searchRRatedMovie an die searchMovie-Abfrage-Resolver-Implementierung.

public class QueryResolver  {

  @Autowired
  MovieRepository repository;

  public Movie searchRRatedMovie(String title, DataFetchingEnvironment environment) {
    return this.searchMovie(name, "R", environment);
  }

  public Movie searchMovie(String title, String rating, DataFetchingEnvironment environment) {
    if(!environment.containsArgument("rating")) {
      //if the rating argument was omitted from the query
      return repository.findByTitle(title);
    } else if(rating == null) {
      //rating is an argument but was set to null (ie. the user wants to retrieve all the movies without any rating)
      return repository.findByTitleAndRating(title, null);
    } else {
      repository.findByNameAndTitle(name,rating);
    }
  }

}

Das sieht gut aus, aber die Abfrage gibt nicht null zurück.

Der erste Resolver ruft an searchRRatedMovie("NotRRatedMovie", environment). Die Umgebung enthält kein "rating"Argument. Beim Erreichen der Zeile: if(!environment.containsArgument("rating")) {Das "rating"Argument ist nicht vorhanden und gibt die if-Anweisung ein, wobei repository.findByTitle("NotRRatedMovie")anstelle der erwarteten zurückgegeben wird repository.findByTitleAndRating("NotRRatedMovie","R").

Beispiel 2: Mutation mit Teilaktualisierungen

Wir können die DataFetchingEnvironment-Argumente verwenden, um Teilaktualisierungen in einer Mutation zu implementieren: Wenn ein Argument vorhanden ist null, benötigen wir die DataFetchingEnvironment-Argumente, um uns mitzuteilen, ob das Argument auf nullfestgelegt wurde null(dh die Mutation sollte den zugrunde liegenden Wert auf aktualisieren null) oder darauf wurde überhaupt nicht gesetzt (dh die Mutation sollte den zugrunde liegenden Wert nicht aktualisieren).

public class MutationResolver  {

  @Autowired
  MovieRepository movieRepository;

  @Autowired
  ActorRepository actorRepository;

  public Movie updateMovie(Long id, String title, List<ActorUpdateInput> actors, DataFetchingEnvironment environment) {
    Movie movie = movieRepository.findById(id);

    //Update the title if the "title" argument is set
    if(environment.containsArgument("title")) {
      movie.setTitle(title);
    }

    if(environment.containsArgument("actors")) {
      for(ActorUpdateInput actorUpdateInput : actors) {
        //The passing the environment happens here
        this.updateActor(actorUpdateInput, environment);
      }
    }
    return movie;
  }

  public Actor updateActor(ActorUpdateInput input, DataFetchingEnvironment environment) {
    Actor actor = actorRepository.findById(input.getId());

    //We retrieve the argument "input". It is a Map<String, Object> where keys are arguments of the ActorUpdateInput
    Map<String, Object> actorArguments = (Map<String, Object>) env.getArguments().get("input");
  
    //Problem: if the environment was passed from updateMovie, it does not contains an "input" parameter! actorArguments is now null and the following code will fail

    //Update the actor name if the "name" argument is set
    if (actorArguments.containsKey("name")) {
      actor.setName(input.getName());
    }

    //Update the actor role if the "role" argument is set
    if (actorArguments.containsKey("role")) {
      actor.setRole(input.getRole());
    }
    return actor;
  }

}

Hier erwartete der UpdateActor-Resolver ein Eingabeargument (das mit der UpdateActor-Mutationsdefinition übereinstimmen würde). Da wir eine falsch besetzte Umgebung passiert haben, ist die Implementierung unterbrochen.

Lösung

Teilaktualisierungen ohne DataFetchinEnvironment

Wenn Sie Teilaktualisierungen implementieren möchten, können Sie dies tun, ohne die DataFecthingEnvironment zu verwenden, wie ich es in diesem Kommentar getan habe: https://github.com/graphql-java-kickstart/graphql-java-tools/issues/141#issuecomment-560938020

Erstellen Sie die DataFetchingEnvironment neu, bevor Sie sie an den nächsten Resolver übergeben

Wenn Sie die DataFetchingEnvironment wirklich benötigen, können Sie dennoch eine neue erstellen, die an den nächsten Resolver übergeben wird. Dies wird wahrscheinlich schwieriger und fehleranfälliger, aber Sie können sich ansehen, wie die ursprüngliche DataFetchingEnvironment in ExecutionStrategy.java erstellt wirdhttps://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/execution/ExecutionStrategy.java#L246