Short answer
You can delegate your resolver calls to the service layer, but do not pass the DataFecthingEnvironment between resolvers/services. It would not be correctly populated.
Long answer
It is not safe and it could result in bugs difficult to pinpoint and data losses.
The DataFetchingEnvironment is populated from the graphql query/mutation being performed, and you would expect the DataFetchingEnvironment in your resolver method to be consistent with the resolver method being called.
Consider the schema below:
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
}
Example 1: Query
query {
searchRRatedMovie(name: "NotRRatedMovie") {
title
}
}
The movie "NotRRatedMovie" is not R rated, we can expect this query to return a null data.
Now, the implementation below passes the DataFetchingEnvironment from the searchRRatedMovie to the searchMovie query resolver implementation.
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);
}
}
}
That looks good, but the query will not return null.
The first resolver will call searchRRatedMovie("NotRRatedMovie", environment)
. The environment does not contain a "rating"
argument. When reaching the line: if(!environment.containsArgument("rating")) {
the "rating"
argument is not present and it will enter the if statement, returning repository.findByTitle("NotRRatedMovie")
instead of the expected repository.findByTitleAndRating("NotRRatedMovie","R")
.
Example 2: Mutation with partial updates
We can use the DataFetchingEnvironment arguments to implement partial updates in a mutation: if an argument is null
we need the DataFetchingEnvironment arguments to tell us if the argument is null
because it was set to null
(ie. the mutation should update the underlying value to null
) or because it was not set at all (ie. the mutation should not update the underlying value).
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;
}
}
Here the updateActor resolver expected an input argument (that would match the updateActor mutation definition). Because we passed a wrongly populated environment, the implementation broke.
Solution
Partial updates without DataFetchinEnvironment
If you want to implement partial updates, you can do so without using the DataFecthingEnvironment, as I did in this comment: https://github.com/graphql-java-kickstart/graphql-java-tools/issues/141#issuecomment-560938020
Rebuild the DataFetchingEnvironment before passing it to the next resolver
If you really need the DataFetchingEnvironment, you can still build a new one to pass to the next resolver. This is gonna be probably more difficult and error prone but you can have a look at how the original DataFetchingEnvironment is created in ExecutionStrategy.java https://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/execution/ExecutionStrategy.java#L246