我可以安全地从GraphQLResolver回调GraphQLQueryResolver吗? [英] Can I safely call back a GraphQLQueryResolver from a GraphQLResolver?

查看:148
本文介绍了我可以安全地从GraphQLResolver回调GraphQLQueryResolver吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的问题是:实现 resolution 方法的最佳方法是什么?直接调用数据存储库还是调用主解析器,又称一个实现 GraphQLQueryResolver 的方法(只要它具有适当的方法)?换句话说(请参见下面的示例),在调用主解析器时,是否已正确调整/设置 DataFetchingEnvironment ?

注意:如果您不熟悉GraphQL Java工具的 Resolvers 的工作方式,那么让我来看一下@ https://www.graphql-java-kickstart.com/tools/schema-definition/

现在是示例.

在Spring Boot应用程序中,使用GraphQL Java工具(具有 graphql-spring-boot-starter 依赖性),让我们拥有以下架构:

 类型用户{ID:ID名称:字符串公司:公司}公司类型{ID:ID名称:字符串} 

具有匹配的POJO或实体(省略了getter/setter):

  class用户{私人Long ID;私有字符串名称;私人Long idCompany;}班级公司{私人Long ID;私有字符串名称;} 

和这些 resolvers (注意:UserRepository和CompanyRepository是您常用的DAO/Repository类类,由Spring Data(JPA)支持,其他某种东西或您自己的自定义实现,无论如何...):

  QueryResolver实现GraphQLQueryResolver {@Autowired私人UserRepository userRepository;@Autowired私人CompanyRepository公司存储库;公共用户用户(字符串ID){返回userRepository.findById(id);}上市公司company(String idCompany){return companyRepository.findById(idCompany);}}UserResolver实现GraphQLResolver< User>.{@Autowired私人CompanyRepository公司存储库;公众公司公司(用户){return companyRepository.findById(user.getIdCompany());}//...或者我应该这样做:@Autowired私有QueryResolver queryResolver;公众公司公司(用户){返回queryResolver.company(user.getIdCompany());}} 

在每个方法的末尾添加 DataFetchingEnvironment环境并在执行对各种(数据)存储库的调用之前使用它,这(更)有意义.

继续上面的示例,这样做是否正确(即 DataFetchingEnvironment 在再次传输到主QueryResolver时是否正确填充)?

  UserResolver实现GraphQLResolver< User>{@Autowired私有QueryResolver queryResolver;上市公司(用户,DataFetchingEnvironment环境){返回queryResolver.company(user.getIdCompany(),environment);}} 

解决方案

简短答案

您可以将解析程序调用委派给服务层,但不要在解析程序/服务之间传递DataFecthingEnvironment.无法正确填充.

长答案

这是不安全的,并且可能导致难以确定的错误和数据丢失.

从正在执行的graphql查询/突变中填充了DataFetchingEnvironment,并且您希望您的resolver方法中的DataFetchingEnvironment与调用的resolver方法一致.

考虑以下架构:

 类型电影{id:ID!标题:字符串!等级:字符串演员:[演员]}类型演员{id:ID!名称:字符串!角色:字符串}输入ActorUpdateInput {id:ID!名称:字符串角色:字符串}输入查询{#搜索具有指定评分的电影searchMovie(名称:movieTitle,评分:字符串):书#搜索R级电影searchRRatedMovie(名称:movieTitle):书}类型突变{#更新电影及其演员updateMovie(id:Id !,标题:字符串,演员:[ActorUpdateInput]):电影#更新演员updateActor(input:ActorUpdateInput!):Actor} 

示例1:查询

 查询{searchRRatedMovie(名称:"NotRRatedMovie"){标题}} 

电影"NotRRatedMovie"没有R评级,我们可以期望该查询返回空数据.

现在,下面的实现将DataFetchingEnvironment从searchRRatedMovie传递到searchMovie查询解析器实现.

 公共类QueryResolver {@AutowiredMovieRepository存储库;public Movie searchRRatedMovie(字符串标题,DataFetchingEnvironment环境){返回this.searchMovie(name,"R",environment);}公共电影searchMovie(字符串标题,字符串等级,DataFetchingEnvironment环境){if(!environment.containsArgument("rating"))){//如果查询中省略了rating参数返回repository.findByTitle(title);} else if(rating == null){//rating是一个参数,但设置为null(即,用户希望检索所有没有评级的电影)返回repository.findByTitleAndRating(title,null);} 别的 {repository.findByNameAndTitle(name,rating);}}} 

这看起来不错,但查询不会返回null.

第一个解析器将调用 searchRRatedMovie("notRRatedMovie",environment).该环境不包含"rating" 参数.到达以下行时: if(!environment.containsArgument("rating")){不存在"rating" 参数,它将进入if语句,返回 repository.findByTitle("NotRRatedMovie")而不是预期的 repository.findByTitleAndRating("NotRRatedMovie","R").

示例2:部分更新的变异

我们可以使用DataFetchingEnvironment参数在突变中实现部分更新:如果参数为 null ,我们需要DataFetchingEnvironment参数来告诉我们参数是否为 null ,因为或将其设置为 null (即,该突变应将基础值更新为 null )或因为它根本没有设置(即,该突变应不更新基础值)值).

 公共类MutationResolver {@AutowiredMovieRepository movieRepository;@AutowiredActorRepository actorRepository;公共电影updateMovie(长ID,字符串标题,List< ActorUpdateInput>演员,DataFetchingEnvironment环境){电影movie = movieRepository.findById(id);//如果标题为"title",则更新标题设置了参数if(environment.containsArgument("title"))){movie.setTitle(title);}if(environment.containsArgument("actors")){for(ActorUpdateInput actorUpdateInput:actors){//传递环境发生在这里this.updateActor(actorUpdateInput,environment);}}返回电影;}公共Actor updateActor(ActorUpdateInput输入,DataFetchingEnvironment环境){Actor actor = actorRepository.findById(input.getId());//我们检索参数"input".它是Map< String,Object>其中键是ActorUpdateInput的参数Map< String,Object>actorArguments =(Map< String,Object>)env.getArguments().get("input");//问题:如果环境是从updateMovie传递的,则它不包含"input".范围!actorArguments现在为空,并且以下代码将失败//如果名称"为"name",则更新演员名称.设置了参数如果(actorArguments.containsKey("name"))){actor.setName(input.getName());}//如果角色"role"是0,则更新演员角色.设置了参数如果(actorArguments.containsKey("role"))){actor.setRole(input.getRole());}返回演员;}} 

在这里,updateActor解析器期望输入参数(它将与updateActor突变定义匹配).因为我们通过了一个错误填充的环境,所以实现失败了.

解决方案

没有DataFetchinEnvironment的部分更新

如果要实现部分更新,则可以不使用DataFecthingEnvironment来实现,就像我在此注释中所做的那样:https://www.graphql-java-kickstart.com/tools/schema-definition/

Now the example.

In a Spring Boot app, with GraphQL Java Tools (with the graphql-spring-boot-starter dependency), let's have this schema:

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

type Company {
  id: ID
  name: String
}

with matching POJO or entity (getters/setters are omitted):

class User {

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

}

class Company {

  private Long id;
  private String name;

}

and these resolvers (Note: UserRepository and CompanyRepository are your usual DAO/Repository-kind-of-classes, either backed by Spring Data (JPA), something else or your own custom implementation, whatever...):

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

}

This makes (more) sense when adding DataFetchingEnvironment environment at the end of each method, AND using it before performing the calls to the various (data) repositories.

Continuing with the example above, would it be correct to do this (i.e. would the DataFetchingEnvironment be properly populated when transmitted again to the main QueryResolver)?

UserResolver implements GraphQLResolver<User> {

  @Autowired
  private QueryResolver queryResolver;

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

}

解决方案

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

这篇关于我可以安全地从GraphQLResolver回调GraphQLQueryResolver吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆