GraphQL的基本使用

GraphQL提供了一种统一的API接口,允许客户端按需获取数据,避免冗余调用。在Java环境中,Netflix的DGS框架简化了GraphQL的实现,通过定义.graphqls文件和使用Resolver、DataLoader等概念,实现了数据的灵活获取和异步加载,优化了性能。DGS还提供了与SpringBoot的集成,便于开发和配置。
摘要由CSDN通过智能技术生成

基础知识

概述

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

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

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

优势

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

在这里插入图片描述

实现

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

关键字

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

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

快速开始

选型

graphql的java实现有多种:

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

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

引入依赖

dependencyManagement-dependencies:

<dependency>
	<groupId>com.netflix.graphql.dgs</groupId>
	<artifactId>graphql-dgs-platform-dependencies</artifactId>
	<version>4.9.16</version>
	<type>pom</type>
	<scope>import</scope>
</dependency>

dependencies:

<dependency>
	<groupId>com.netflix.graphql.dgs</groupId>
	<artifactId>graphql-dgs-spring-boot-starter</artifactId>
</dependency>
数据类型定义

​ 一个用户可以有多个手机,是一种一对多的关系。我们定义一个用户手机查询的接口,返回值包含用户所拥有的手机列表。在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<User> userList = new ArrayList<>();
    //手机列表集合
    public static List<Phone> 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<User> userQuery(@InputArgument("userIdList")List<String> userIdList){
        List<User> 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<User> userList){

        List<Phone> list = new ArrayList<>();


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

        for(User a: userList){
            if(map.containsKey(a.getUserId())){
                List<Phone> 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<Phone> getPhone(DgsDataFetchingEnvironment dfe){
        System.out.println("2-同步获取用户手机信息被执行");

        List<Phone> 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<Phone> PhoneList(DgsDataFetchingEnvironment dfe){

        User User = dfe.getSource();
        DataLoader<String,Phone> dataLoader = dfe.getDataLoader(PhoneDataLoader.class);
        return dataLoader.load(User.getUserId());

    }

}

实现数据获取类PhoneDataLoader

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

    //用户列表集合
    public static List<User> userList = new ArrayList<>();
    //手机列表集合
    public static List<Phone> 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<List<List<Phone>>> load(List<String> list) {

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

    }

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

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

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

        return resultList;

    }

}
发起测试

运行项目,访问http://localhost:8080/graphiql,会出现如下所示的界面。将入参放入左侧点击查询即可。
在这里插入图片描述

备注

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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值