zoukankan      html  css  js  c++  java
  • GraphQL学习笔记

    说明

    dgs官网
    建议关注dgs的更新,能访问官网尽量看官网,github代码一般不会是最新的


    dgs-framework-github

    GraphQL 自带一组默认标量类型

    • Int:有符号 32 位整数。
    • Float:有符号双精度浮点值。
    • String:UTF‐8 字符序列。
    • Boolean:true 或者 false。
    • ID:ID 标量类型表示一个唯一标识符,通常用以重新获取对象或者作为缓存中的键。ID 类型使用和 String 一样的方式序列化;然而将其定义为 ID 意味着并不需要人类可读型。


    代码见gitee仓库example

    GraphQL操作类型

    • query 查询
    • mutation 突变(理解为修改即可)
    • subscription 脚本

    dgs自定义scalar

    • 实现接口Coercing
    • 添加注解@DgsScalar
      示例
    @DgsScalar(name = "Date")
    public class DateCoercing implements Coercing<Date, String> {
        private static final String PATTERN = "yyyy-MM-dd HH:mm:ss";
    
        @Override
        public String serialize(Object o) throws CoercingSerializeException {
            if (o instanceof Date && Objects.nonNull(o)) {
                Date date = (Date)o;
                SimpleDateFormat sdf = new SimpleDateFormat(PATTERN);
                return sdf.format(date);
            }
            return null;
        }
    
        @Override
        public Date parseValue(Object o) throws CoercingParseValueException {
            if (o instanceof Date && Objects.nonNull(o)) {
                return (Date)o;
            } else {
                throw new CoercingParseValueException("type is not date ");
            }
        }
    
        @Override
        public Date parseLiteral(Object o) throws CoercingParseLiteralException {
            if (o instanceof String && Objects.nonNull(o)) {
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PATTERN);
                LocalDateTime localDateTime = LocalDateTime.parse(o.toString(), formatter);
                return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
            }
            return null;
        }
    }
    
    .graphql配置
    type Show {
        title: String
        releaseYear: Int
        now: Date
    }
    scalar Date
    

    postman访问

    别名

    当两个查询名相同时会引发冲突,这时可以使用别名获取不同的结果

    {
      zhangsan: hello(name: "张三"),
      lisi: hello(name: "李四")
    }
    输出:
    {
        "data": {
            "zhangsan": "hello:张三!",
            "lisi": "hello:李四!"
        }
    }
    

    片段(fragment)

    片段主要用于将多个相同的字段进行封装,我们在实际开发中会遇到返回结果中很多字段名名相同,使用fragment在.graphql文件中定义

    {
      golang: shows (title: "golang"){
        ...showFields
      },
      java: shows (title: "java"){
        ...showFields
      }
     }
     
     # 定义片段
    fragment showFields on Show {
      title
      releaseYear
      now
    }
    输出
    {
        "data": {
             "golang": [
          {
            "title": "golang",
            "releaseYear": 2009,
            "now": "2021-03-08 13:46:43"
          }
        ],
        "java": [
          {
            "title": "java",
            "releaseYear": 1995,
            "now": "2021-03-08 13:46:43"
          }
        ]
        }
    }
    
    

    上面的on后面的类型必须在.graphql文件中有定义

    多个.graphqls的使用

    自定义address.graphqls

    
    # 多个.graphqls定义需要使用这种方式
    extend type Query {
        listAddress(id: Int): Address
    }
    
    extend type Mutation {
        addAddress(input: AddressInput): Int
    }
    
    type Address {
        id: Int
        street: String
        city: String
        country: String
        customerId: Int
    }
    
    input AddressInput {
        street: String
        city: String
        country: String
        customerId: Int
    }
    

    自定义的其他的schema文件查询需要使用type extend 具体类型方式

    Mutation

    mutation用于修改对象,dgs使用注意事项

    • @DgsData中的parentType="Mutation",field需要与schema中的保持一致
    • 对象类型在java中定义的同时,需要在对应的.graphqls文件中定义,类型是input
    input AddressInput {
        street: String
        city: String
        country: String
        customerId: Int
    }
    
    • graphiql查询页面中的对象要放在变量中定义
    查询输入框的示例
    添加以对象格式传参
    mutation AddAddress($addAddress: AddressInput) {
      	addAddress(input: $addAddress)
    }
    修改的请求参数直接以参数形式传递
    mutation UpdateAddress {
      updateAddress(id: 1, street: "凤阳街道", city: "广州") {
        id
        street
        city
        customerId
      }
    }
    query variables中的变量值
    {
      "addAddress": {
      	"street": "江海街道", "city": "广州", "country": "中国", "customerId": 1
    	}
    }
    


    变更中的多个字段(Multiple fields in mutations)
    一个变更也能包含多个字段,一如查询。查询和变更之间名称之外的一个重要区别是:

    • 非变量定义方式
    mutation AddAddress1 {
      addAddress(input: {    
        street: "昌岗街道",
        city: "广州",
        country: "中国",
        customerId: 1
      
      })   
    }
    ![](https://gitee.com/sharehappy/figurebed/raw/master/md/img/2021/04/20210421183524.png)
    

    查询字段时,是并行执行,而变更字段时,是线性执行,一个接着一个。

    操作名称

    上面我们主要是查询,省略了query关键字和查询名称,生产中加上这些可以减少代码歧义

    query Abc {
      hello(name: "zhangsan")
    }
    
    mutation AddAddress($addAddress: AddressInput) {
      	addAddress(input: $addAddress)
    }
    
    mutation UpdateAddress {
      updateAddress(id: 1, street: "凤阳街道", city: "广州") {
        id
        street
        city
        customerId
      }
    }
    
    1. 操作类型可以是 query、mutation 或 subscription,描述你打算做什么类型的操作。操作类型是必需的,除非你使用查询简写语法,在这种情况下,你无法为操作提供名称或变量定义。
    2. 操作名称是你的操作的有意义和明确的名称。它仅在有多个操作的文档中是必需的,但我们鼓励使用它,因为它对于调试和服务器端日志记录非常有用。 当在你的网络或是 GraphQL 服务器的日志中出现问题时,通过名称来从你的代码库中找到一个查询比尝试去破译内容更加容易。 就把它想成你喜欢的程序语言中的函数名

    变量(Variables)

    前面都是基于多个参数查询,为了扩展减少修改代码,我们通常将多个参数封装成对象。GraphQL 拥有一级方法将动态值提取到查询之外,然后作为分离的字典传进去。这些动态值即称为变量

    使用变量之前,我们得做三件事:

    • 使用 $variableName 替代查询中的静态值。
    • 声明 $variableName 为查询接受的变量之一。
    • 将 variableName: value 通过传输专用(通常是 JSON)的分离的变量字典中。

    示例:

    添加地址的变量
    mutation AddAddress($addAddress: AddressInput) {
      	addAddress(input: $addAddress)
    }
    输入框中的实际变量值
    {
      "addAddress": {
      	"street": "江海街道", "city": "广州", "country": "中国", "customerId": 1
    	}
    }
    
    

    变量定义(Variable definitions)

    变量定义看上去像是上述查询中的 ($addAddress: AddressInput)。其工作方式跟类型语言中函数的参数定义一样。它以列出所有变量,变量前缀必须为 $,后跟其类型,本例中为 AddressInput。

    所有声明的变量都必须是标量、枚举型或者输入对象类型。所以如果想要传递一个复杂对象到一个字段上,你必须知道服务器上其匹配的类型。可以从Schema页面了解更多关于输入对象类型的信息。

    变量定义可以是可选的或者必要的。上例中,AddressInput 后并没有 !,因此其是可选的。但是如果你传递变量的字段要求非空参数,那变量一定是必要的。

    默认变量(Default variables)

    可以通过在查询中的类型定义后面附带默认值的方式,将默认值赋给变量。

    query HeroNameAndFriends($episode: Episode = "JEDI") {
      hero(episode: $episode) {
        name
        friends {
          name
        }
      }
    }
    

    当所有变量都有默认值的时候,你可以不传变量直接调用查询。如果任何变量作为变量字典的部分传递了,它将覆盖其默认值。

    指令(Directives)

    指令可以用来动态控制是否显示某个字段,有些场景下我们可能需要使用对象的A属性,一些场景不需要A属性,可以使用指令控制

    query movies($withDirector: Boolean!) {
      movies {
        title
        director @skip(if: $withDirector)
        actor @include (if: $withDirector) {
        	name
        } 
        actor {      
          home
        }
        
      }
    }
    变量
    {
      "withDirector": false
    }
    
    

    变量值为true

    变量值为false

    对于字段操作可以使用@skip,如果对对象中的某些属性操作可以使用@include

    我们用了 GraphQL 中一种称作指令的新特性。一个指令可以附着在字段或者片段包含的字段上,然后以任何服务端期待的方式来改变查询的执行。GraphQL 的核心规范包含两个指令,其必须被任何规范兼容的 GraphQL 服务器实现所支持:

    • @include(if: Boolean) 仅在参数为 true 时,包含此字段。
    • @skip(if: Boolean) 如果参数为 true,跳过此字段。
      指令在你不得不通过字符串操作来增减查询的字段时解救你。服务端实现也可以定义新的指令来添加新的特性。

    内联片段(inline fragment)

    如果你查询的字段返回的是接口或者联合类型,那么你可能需要使用内联片段来取出下层具体类型的数据:

    /**
     * 根据枚举类型不同获取不同类型的结果,graphql中使用的是内联片段
     * @param filter
     * @return
     */
    @DgsData(parentType = "Query", field = "movieFilter")
    public List<Movie> movieFilter(@InputArgument("filter")MovieType filter) {
        return initMovie(filter);
    }
    private List<Movie> initMovie(MovieType movieType) {
        List<Movie> movies = new ArrayList<>();
        Actor actor1 = new Actor();
        actor1.setHome("广州");
        actor1.setName("张三");
        Actor actor2 = new Actor();
        actor2.setHome("上海");
        actor2.setName("李四");
        if (movieType.equals(MovieType.Action) || movieType.equals(MovieType.All)) {
            movies.add(new ActionMovie("Crouching Tiger", null, 0, actor1));
            movies.add(new ActionMovie("Black hawk down", null, 10, actor1));
        }
        if (movieType.equals(MovieType.Scary) || movieType.equals(MovieType.All)) {
            movies.add(new ScaryMovie("American Horror Story", null, true, 10, actor2));
            movies.add(new ScaryMovie("Love Death + Robots", null, false, 4, actor2));
        }
        return movies;
    }    
    

    schema

    # 定义枚举
    enum MovieType {
        Scary
        Action
        All
    }
    
    type Query {
        movieFilter(filter: MovieType): [Movie]
    }
    
    graphiql查询
    
    
    

    元字段(Meta fields)

    某些情况下,你并不知道你将从 GraphQL 服务获得什么类型,这时候你就需要一些方法在客户端来决定如何处理这些数据。GraphQL 允许你在查询的任何位置请求 __typename,一个元字段,以获得那个位置的对象类型名称


    schema和类型

    枚举

    枚举类型是一种特殊的标量,它限制在一个特殊的可选值集合内。

    • 验证这个类型的任何参数是可选值的的某一个
    • 与类型系统沟通,一个字段总是一个有限值集合的其中一个值。
    enum Episode {
      NEWHOPE
      EMPIRE
      JEDI
    }
    

    接口(Interfaces)

    跟许多类型系统一样,GraphQL 支持接口。一个接口是一个抽象类型,它包含某些字段,而对象类型必须包含这些字段,才能算实现了这个接口。

    interface Movie {
        title: String
        director: String
        actor: Actor
    }
    
    type ScaryMovie implements Movie {
        title: String
        director: String
        gory: Boolean
        scareFactor: Int
        actor: Actor
    }
    
    type ActionMovie implements Movie {
        title: String
        director: String
        nrOfExplosions: Int
        actor: Actor
    }
    

    联合类型(union)

    联合类型和接口十分相似,但是它并不指定类型之间的任何共同字段。

    union MovieResult = ScaryMovie | ActionMovie
    

    在我们的schema中,任何返回一个 MovieResult 类型的地方,都可能得到一个 ScaryMovie、ActionMovie p。注意,联合类型的成员需要是具体对象类型;你不能使用接口或者其他联合类型来创造一个联合类型。

    这时候,如果你需要查询一个返回 SearchResult 联合类型的字段,那么你得使用条件片段才能查询任意字段。

    schema
    # union 联合类型
    movieFilter(filter: MovieType): [MovieResult]
    
    查询
    query Movie($filter: MovieType!) {
      movieFilter(filter: $filter) {
        __typename
        ... on ScaryMovie {
          scareFactor
          title
          director
          actor {
            name
            home
          }
        }
        ... on ActionMovie {
          nrOfExplosions
          title
          director
          actor {
            name
            home
          }
        }
      }
    }
    

    联合类型可以理解为根据不同的参数返回不同类型数据的组合,与接口稍有不同,可以对比schema和查询结果

    graph-java中的Instrumentation

    实现instrumentation接口或者继承SimpleInstrumentation可以进行日志、权限处理,类似spring中的aop

    @Configuration
    public class GraphQLConfig {
    
        @Bean
        @Primary
        public Instrumentation instrumentation() {
            Instrumentation instrumentation = new ChainedInstrumentation(new TracingInstrumentation(), new CustomInstrumentation());
            return instrumentation;
        }
    
    }
    
    

    加@Primary是由于DgsAutoConfiguration.kt中有对应的bean注入了

    分页

    graphql中的分页只能知道是否还有下一页数据,不能获取总量和当前页,这种结果可以结合dataLoader缓存根据条件查询滚动翻页
    schema.graphqls

    ### 分页相关
    type AddressConnection {
        edges: [AddressEdge]
        pageInfo: PageInfo
    }
    
    type AddressEdge {
        cursor: String
        node: Address
    }
    
    type PageInfo {
        hasPreviousPage: Boolean!
        hasNextPage: Boolean!
    }
    
        /**
         * 分页查询
         * @param environment
         * @param customerId 查询条件
         * @param first 后面的的多少条结果
         * @param after 每条数据都有游标,当前游标后的几条数据 与first配合使用,null则从第一条开始
         * @param last 前面多少条数据
         * @param before  当前游标前的几条数据 与last配合使用,null则从最后一条开始
         * @return
         */
        @DgsData(parentType = "Query", field = "pageAddress")
        public Connection<Address> pageAddress(DataFetchingEnvironment environment, @InputArgument("customerId")Integer customerId, @InputArgument("first")Integer first,
                                               @InputArgument("after")String after, @InputArgument("last")Integer last, @InputArgument("before")String before) {
            List<Address> addressList = addressMapper.selectList(Wrappers.<Address>lambdaQuery().eq(Address::getCustomerId, customerId));
            return new SimpleListConnection(addressList).get(environment);
        }
    

    @InputArgument

    GraphQL中的输入参数,允许的参数类型

    • input type
    • scalar
    • enum

    对象嵌套查询

    # java代码
    /**
     * 嵌套查询
     * @param dfe
     * @return
     */
    @DgsData(parentType = "Show", field = "actors")
    public List<Actor> actors(DgsDataFetchingEnvironment dfe) {
        Show show = dfe.getSource();
        return actorService.forShow(show.getId());
    }
    
    # schema    
    type Show {
        id: Int
        title: String
        releaseYear: Int
        now: Date
        actors: [Actor]
    }
    

    @InputArgument with lists

    java代码

    /**
     *  参数为集合时,需要使用collectionType指定类型 @@任意对象类型@@
     */
    @DgsQuery(field = "showsById")
    public List<Show> showsById(@InputArgument(value = "idList", collectionType = Integer.class) List<Integer> idList) {
        return shows.stream().filter(a -> idList.contains(a.getId())).collect(Collectors.toList());
    }
    

    schema

    """list参数"""
    showsById(idList: [Int]): [Show]
    
    

    DgsRequestData

    @DgsData(parentType = "Query", field = "serverName")
    public String serverName(DgsDataFetchingEnvironment dfe) {
        DgsWebMvcRequestData requestData = (DgsWebMvcRequestData)DgsContext.getRequestData(dfe);
        HttpServletRequest request = ((ServletWebRequest) requestData.getWebRequest()).getRequest();
        return request.getServerName() + "::" + request.getPathInfo();
    }
    
    # schema
    serverName: String
    

    N+1 问题

    例如:查询一个movie列表,但是每个movie都包含了director,比如50个movie,需要加载director50次,加载movie列表一次总共就是51次,这种性能肯定不好,我们可以独立一个服务查询director列表,然后每个movie去director列表过滤对应director,此时我们可以使用dataLoader

    BatchLoader

    通常返回List

    @DgsDataLoader(name = "actors")
    public class ActorsDataLoader implements BatchLoader<String, Actor> {
        @Autowired
        private ActorService actorService;
    
        @Override
        public CompletionStage<List<Actor>> load(List<String> list) {
            return CompletableFuture.supplyAsync(() -> actorService.listActors(list));
        }
    }
    

    MappedBatchLoader

    通常返回Map数据

    @DgsDataLoader(name = "actorsMap")
    public class ActorsMapDataLoader implements MappedBatchLoader<String, Actor> {
    
        @Autowired
        private ActorService actorService;
    
        @Override
        public CompletionStage<Map<String, Actor>> load(Set<String> set) {
            return CompletableFuture.supplyAsync(() -> actorService.mapActors(set));
        }
    }
    
    

    错误处理

    • 自定义异常
    public class MyException extends RuntimeException {
    }
    
    • 实现接口DataFetcherExceptionHandler,DefaultDataFetcherExceptionHandler处理异常
    @Component
    public class CustomDataFetchingExceptionHandler implements DataFetcherExceptionHandler {
        private final DefaultDataFetcherExceptionHandler defaultHandler = new DefaultDataFetcherExceptionHandler();
    
        @Override
        public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) {
    
            if (handlerParameters.getException() instanceof MyException) {
                Map<String, Object> debugInfo = new HashMap<>();
                debugInfo.put("somefield", "somevalue");
    
                GraphQLError graphqlError = TypedGraphQLError.INTERNAL.message("This custom thing went wrong!")
                        .debugInfo(debugInfo)
                        .path(handlerParameters.getPath()).build();
                return DataFetcherExceptionHandlerResult.newResult()
                        .error(graphqlError)
                        .build();
            } else {
                return defaultHandler.onException(handlerParameters);
            }
        }
    }
    
    
    • 应用
     @DgsQuery(field = "errors")
        @DgsEnableDataFetcherInstrumentation(value = false)
        public String errors(DataFetchingEnvironment dfe) {
            throw new MyException();
        }
    

    安全集成spring security

    Postman 访问

    参考:https://blog.csdn.net/chen_duochuang/article/details/107211306

    graphql中文官网

  • 相关阅读:
    安装lnmp 时如何修改数据库数据存储地址及默认访问地址
    ubuntu 设置root用户密码并实现root用户登录
    解决ubuntu 远程连接问题
    linux 搭建FTP服务器
    PHP 根据ip获取对应的实际地址
    如何发布自己的composer包
    使用composer安装composer包报Your requirements could not be resolved to an installable set of packages
    laravel 框架配置404等异常页面
    使用Xshell登录linux服务器报WARNING! The remote SSH server rejected X11 forwarding request
    IoTSharp 已支持国产松果时序数据库PinusDB
  • 原文地址:https://www.cnblogs.com/javashare/p/15077397.html
Copyright © 2011-2022 走看看