Posso richiamare in sicurezza un GraphQLQueryResolver da un GraphQLResolver?
La mia domanda è: qual è il migliore quando si implementano metodi di risoluzione ? chiamata diretta di repository di dati o richiamo di un risolutore principale noto anche come implementazione GraphQLQueryResolver(a condizione che abbia metodi appropriati)? in altre parole (vedere l'esempio sotto), il valore è DataFetchingEnvironmentregolato / impostato correttamente quando si richiama un resolver principale ?
Nota: se non hai familiarità con come Resolverslavorare con GraphQL Java Tools, ti farò dare un'occhiata @https://www.graphql-java-kickstart.com/tools/schema-definition/
Ora l'esempio.
In un'app Spring Boot, con GraphQL Java Tools (con la graphql-spring-boot-starterdipendenza), disponiamo di questo schema:
type User {
id: ID
name: String
company: Company
}
type Company {
id: ID
name: String
}
con POJO o entità corrispondenti (i getter / setter sono omessi):
class User {
private Long id;
private String name;
private Long idCompany;
}
class Company {
private Long id;
private String name;
}
e questi risolutori (Nota: UserRepository e CompanyRepository sono le tue solite classi DAO / Repository, supportate da Spring Data (JPA), qualcos'altro o la tua implementazione personalizzata, qualunque sia ...):
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());
}
}
Questo ha (più) senso quando si aggiunge DataFetchingEnvironment environmentalla fine di ogni metodo E lo si utilizza prima di eseguire le chiamate ai vari repository (di dati).
Continuando con l'esempio precedente, sarebbe corretto farlo (cioè verrebbe DataFetchingEnvironmentcorrettamente popolato quando viene trasmesso di nuovo al QueryResolver principale)?
UserResolver implements GraphQLResolver<User> {
@Autowired
private QueryResolver queryResolver;
public Company company(User user, DataFetchingEnvironment environment) {
return queryResolver.company(user.getIdCompany(), environment);
}
}
Risposte
Risposta breve
È possibile delegare le chiamate del resolver al livello di servizio, ma non passare il DataFecthingEnvironment tra resolver / servizi. Non verrebbe popolato correttamente.
Risposta lunga
Non è sicuro e potrebbe causare bug difficili da individuare e perdite di dati.
DataFetchingEnvironment viene popolato dalla query / mutazione graphql eseguita e ti aspetteresti che DataFetchingEnvironment nel tuo metodo resolver sia coerente con il metodo resolver chiamato.
Considera lo schema seguente:
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
}
Esempio 1: query
query {
searchRRatedMovie(name: "NotRRatedMovie") {
title
}
}
Il film "NotRRatedMovie" non è classificato R, possiamo aspettarci che questa query restituisca dati nulli.
Ora, l'implementazione seguente passa il DataFetchingEnvironment da searchRRatedMovie all'implementazione del risolutore di query searchMovie.
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);
}
}
}
Sembra buono, ma la query non restituirà null.
Il primo resolver chiamerà searchRRatedMovie("NotRRatedMovie", environment). L'ambiente non contiene "rating"argomenti. Quando si raggiunge la riga: if(!environment.containsArgument("rating")) {l' "rating"argomento non è presente e entrerà nell'istruzione if, restituendo repository.findByTitle("NotRRatedMovie")invece del previsto repository.findByTitleAndRating("NotRRatedMovie","R").
Esempio 2: mutazione con aggiornamenti parziali
Possiamo usare gli argomenti DataFetchingEnvironment per implementare aggiornamenti parziali in una mutazione: se un argomento è, nullabbiamo bisogno degli argomenti DataFetchingEnvironment per dirci se l'argomento è nullperché era impostato su null(cioè la mutazione dovrebbe aggiornare il valore sottostante a null) o perché non è stato impostato affatto (cioè la mutazione non dovrebbe aggiornare il valore sottostante).
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;
}
}
Qui il resolver updateActor si aspettava un argomento di input (che corrispondesse alla definizione della mutazione updateActor). Poiché abbiamo superato un ambiente popolato in modo errato, l'implementazione non è riuscita.
Soluzione
Aggiornamenti parziali senza DataFetchinEnvironment
Se vuoi implementare aggiornamenti parziali, puoi farlo senza utilizzare DataFecthingEnvironment, come ho fatto in questo commento: https://github.com/graphql-java-kickstart/graphql-java-tools/issues/141#issuecomment-560938020
Ricostruire il DataFetchingEnvironment prima di passarlo al successivo resolver
Se hai davvero bisogno del DataFetchingEnvironment, puoi comunque crearne uno nuovo da passare al successivo resolver. Questo sarà probabilmente più difficile e soggetto a errori, ma puoi dare un'occhiata a come viene creato il DataFetchingEnvironment originale in ExecutionStrategy.javahttps://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/execution/ExecutionStrategy.java#L246