GraphQL的基本使用

基础知识

概述

​ GraphQL提供了一套前后端数据交互的规范,不同语言可以有自己的GraphQL实现,目前Java已经完成了GraphQL的实现。

​ 使用RESTful风格的API,会从指定接口加载数据。每个接口都明确定义了返回的数据结构。这意味着客户端需要的数据,已经在URL中制定好了。

​ GraphQL的API通常只暴露一个接口,而不是返回固定数据结构的多个接口。 GraphQL返回的数据结构不是一成不变的,而是灵活的,让客户端来决定真正需要的是什么数据。

优势

​ 比如,客户端需要一些数据,我们定义了一个RESTful的接口,但是这些数据分别在A和B服务中,最终我们会在后台手动聚合A和B的数据到一个模型里返回。而GraphQL通过不同的Resolver天然完成了数据聚合功能。当接口调用方不需要B服务的返回字段时,甚至不需要调用B服务,避免冗余调用,增加不必要的接口访问时间和服务方被调用的压力。

GraphQL的基本使用_第1张图片

实现

​ 通过.graphqls文件定义接口和出入参结构后,需要写Resolver类为返回字段赋值,赋值的来源来自于Loader类的load方法。

关键字

​ 在.graphql文件中使用,定义graphql接口,主要有以下几个:

关键字 释义
Query 对GraphQL server发起的只读请求
Mutation 对GraphQL server发起的读-写请求
Resolver 在GraphQL中,Resolver是指后端的请求处理器,它把请求的数据获取映射到后端不同的处理程序上,它类似于RESTFul应用程序中的MVC后端
Type type定义的从服务器端返回的数据表现形式,包含数据字段或者其它type类型
Input Input和Type类似,只是定义了发送到服务器端的数据表现形式,即入参
Scalar Scalar在GraphQL中是基本的数据类型,例如:String, Int, Boolean, Float等等
Interface 接口包含字段的名称及其参数,因此GraphQL类型对象可以从该接口继承,从而确保新类型包含特定的字段
Schema 在GraphQL中,Schema包含了:Query,Mutation,明确了在GraphQL服务器中执行逻辑
Int 32 位有符号整型,超出精度范围后,会抛出异常
Float 有符号双精度浮点数,超出精度范围后,会抛出异常
String 字符串,用于表示 UTF-8 字符序列
Boolean 布尔
ID 资源唯一标志符

快速开始

选型

graphql的java实现有多种:

序号 类型 描述
1 graphql-java graphql的java最原生实现。需要手动写很多东西(schema和对应的实体类,具体的链式调用查询语句等),引入graphiql等多个组件。
2 graphql-kickstart 引入了Resolver类。需要引入graphhiql等多个组件。需要引入多个配置类。
3 graphql-dgs netflix的框架,只需一个依赖就可以引入所有组件,实现了很多注解,开发方便,需要springBoot 2.6.2+以上版本。
4 graphql-spring Spring集成了Graphql。需要jdk17以上的版本。

其中1和2需要手动引入很多依赖,手写很多其实我们不需要关注的类;4需要jdk17以上才能支持,所以我们选择最为方便,最为环境最友好的方式3实现,即graphql-dgs

引入依赖

dependencyManagement-dependencies:


	com.netflix.graphql.dgs
	graphql-dgs-platform-dependencies
	4.9.16
	pom
	import

dependencies:


	com.netflix.graphql.dgs
	graphql-dgs-spring-boot-starter

数据类型定义

​ 一个用户可以有多个手机,是一种一对多的关系。我们定义一个用户手机查询的接口,返回值包含用户所拥有的手机列表。在classPath下创建root.graphql:

type Query {
    #用户手机查询
    userQuery(userIdList:[String]!): [User]
}

type User {
    #用户id
    userId: String
    #用户名称
    userName: String
    #用户的手机列表
    userPhoneList:[Phone]
}

type Phone {
    #手机id
    phoneId: String
    #手机名称
    phoneName: String
}

dgs常用注解

注解 位置 释义
@DgsCompent 标识这个类为dgs的数据查询类
@DgsQuery 方法 标识这是一个dgs查询方法
@DgsData(parentType = “User”,field = “userPhoneList”) 方法 标识这是一个字段解析器(类似于Resolver)。parentType指定父类型,field指定是父类型的某个字段
@DgsDataLoader(name = “userPhoneList”) 标识这个是一个属性获取的类(类似于Loader),通过name字段指定获取的字段名
@InputArgument 参数 标识这个参数对应哪个入参

接口开发

​ .graphql文件定义返回值->@DgsData为返回值进行填充->DgsDataLoader异步获取所需要的返回值。

实现数据查询类UserFetcher

​ 我们在root.graphqls中定义的类型有与之对应的Java Bean,这些Java Bean都提供了getField方法,因此不需要额外实现@DgsData方法去获取对应的属性并且进行赋值。有时候,在type中定义的类型的某个字段数据的获取比较麻烦,不是简单的getField可以解决的(比如用户关联的是一个手机列表),此时可以为此类型实现专门的方法(加@DgsData注解)获取对应的字段值。需要注意的是:客户端的请求返回类型中没有@DgsData注解方法关联的返回值字段,那么该方法将不会被执行

import com.netflix.graphql.dgs.*;
import org.dataloader.DataLoader;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

@DgsComponent
public class UserFetcher {

    //用户列表集合
    public static List userList = new ArrayList<>();
    //手机列表集合
    public static List phoneList = new ArrayList<>();

    static{

        //初始化用户列表集合
        User user1 = new User();
        user1.setUserId("zhangsan");
        user1.setUserName("张三");
        userList.add(user1);

        User user2 = new User();
        user2.setUserId("lisi");
        user2.setUserName("李四");
        userList.add(user2);

        //初始化手机列表集合
        Phone phone1 = new Phone();
        phone1.setPhoneId("huawei");
        phone1.setPhoneName("华为");
        phone1.setUserId("zhangsan");
        phoneList.add(phone1);

        Phone phone2 = new Phone();
        phone2.setPhoneId("xiaomi");
        phone2.setPhoneName("小米");
        phone2.setUserId("zhangsan");
        phoneList.add(phone2);

        Phone phone3 = new Phone();
        phone3.setPhoneId("hongmi");
        phone3.setPhoneName("红米");
        phone3.setUserId("lisi");
        phoneList.add(phone3);
    }

    @DgsQuery
    public List userQuery(@InputArgument("userIdList")List userIdList){
        List list = new ArrayList<>();

        for(User a :userList){
            for(String b:userIdList){
                if(a.getUserId().equals(b)){
                    list.add(a);
                }
            }
        }

        //this.getPhoneInfo(UserInfo);
        System.out.println("姓名查询方法被执行");
        return list;

    }

    /**
     * 原始方法的实现(和java实现没啥区别,体会不出父子对象之间的关系和异步加载)
     * @param
     */
    private void getPhoneInfo(List userList){

        List list = new ArrayList<>();


        Map> map = list.stream().collect(Collectors.groupingBy(Phone::getUserId));

        for(User a: userList){
            if(map.containsKey(a.getUserId())){
                List phones = map.get(a);
                a.setUserPhoneList(phones);
            }
        }

        System.out.println("1-同步获取用户的手机方法被执行");

    }

    /**
     * 前端入参不需要这个返回值,是不会执行查询方法的。
     * 缺点:n+1问题,集合入参会被循环调用。示例入参:
     * @annotation DgsData:字段解析器。parentType:父节点类型 field:父节点对象字段名
     * @param dfe dgs运行期帮我们自动注入的对象,用来拿参数
     * @return
     */
    //@DgsData(parentType = "User",field = "phoneList")
    public List getPhone(DgsDataFetchingEnvironment dfe){
        System.out.println("2-同步获取用户手机信息被执行");

        List resultList = new ArrayList<>();
        User user = dfe.getSource();

        for(Phone a:phoneList){
            if(a.getUserId().equals(user.getUserId())){
                resultList.add(a);
            }
        }
        return resultList;

    }

    /**
     * 前端入参不需要这个返回值,是不会执行查询方法的。与上面的方法不同的是:
     *      1.多线程获取。
     *      2.解决n+1问题。
     * @annotation DgsData:字段解析器 parentType:父节点类型 field:父节点对象字段名
     * @param dfe dgs运行期帮我们自动注入的对象,用来拿参数
     * @return
     */
    @DgsData(parentType = "User",field = "userPhoneList")
    public CompletableFuture PhoneList(DgsDataFetchingEnvironment dfe){

        User User = dfe.getSource();
        DataLoader dataLoader = dfe.getDataLoader(PhoneDataLoader.class);
        return dataLoader.load(User.getUserId());

    }

}

实现数据获取类PhoneDataLoader

​ PhoneDataLoader 需要实现BatchLoader接口。其中第一个参数为查询入参的类型,第二个参数为返回值类型。实现load方法。load方法返回的结果要和第一个参数的结果数量相同,因为要做对应的匹配(即查询的结果是外层查询的某个字段。当用户和手机是一对多的关系时,要对sql的查询结果进行分组赋值,这也是load方法的返回值泛型为什么是List的原因)。

import com.netflix.graphql.dgs.DgsDataLoader;
import org.dataloader.BatchLoader;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;

@DgsDataLoader(name = "userPhoneList")
public class PhoneDataLoader implements BatchLoader> {

    //用户列表集合
    public static List userList = new ArrayList<>();
    //手机列表集合
    public static List phoneList = new ArrayList<>();
    static{

        //初始化用户列表集合
        User user1 = new User();
        user1.setUserId("zhangsan");
        user1.setUserName("张三");
        userList.add(user1);

        User user2 = new User();
        user2.setUserId("lisi");
        user2.setUserName("李四");
        userList.add(user2);

        //初始化手机列表集合
        Phone phone1 = new Phone();
        phone1.setPhoneId("huawei");
        phone1.setPhoneName("华为");
        phone1.setUserId("zhangsan");
        phoneList.add(phone1);

        Phone phone2 = new Phone();
        phone2.setPhoneId("xiaomi");
        phone2.setPhoneName("小米");
        phone2.setUserId("zhangsan");
        phoneList.add(phone2);

        Phone phone3 = new Phone();
        phone3.setPhoneId("hongmi");
        phone3.setPhoneName("红米");
        phone3.setUserId("lisi");
        phoneList.add(phone3);
    }

    /**
     * 两个List嵌套:第一个list为graphql的单个对象到多个对象的转换,第二个list为用户和手机的一对多关系。
     * dgs会把用户id收集起来,一把调用数据库。实现的时候是集合出入参,调用的时候是单个出入参,dgs框架会完成单个和集合的转换。
     * @param list
     * @return
     */
    @Override
    public CompletionStage>> load(List list) {

        return CompletableFuture.supplyAsync(() -> this.getPhoneInfo(list));

    }

    /**
     * 转化一对多的关系
     * @param list
     * @return
     */
    private List> getPhoneInfo(List list){

        System.out.println("3-异步获取用户的手机方法被执行");
        List phones = new ArrayList<>();
        List> resultList = new ArrayList<>();
        for(Phone a :phoneList){
            for(String b:list){
                if(a.getUserId().equals(b)){
                    phones.add(a);
                }
            }
        }
        Map> map = phones.stream().collect(Collectors.groupingBy(Phone::getUserId));

        for(String a: map.keySet()){
            if(map.containsKey(a)){
                List phone = map.get(a);
                resultList.add(phone);
            }
        }

        return resultList;

    }

}

发起测试

运行项目,访问http://localhost:8080/graphiql,会出现如下所示的界面。将入参放入左侧点击查询即可。
GraphQL的基本使用_第2张图片

备注

graphql-dgs文档:https://netflix.github.io/dgs/getting-started/

你可能感兴趣的:(graphql,restful,java)