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后端 |
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
}
注解 | 位置 | 释义 |
---|---|---|
@DgsCompent | 类 | 标识这个类为dgs的数据查询类 |
@DgsQuery | 方法 | 标识这是一个dgs查询方法 |
@DgsData(parentType = “User”,field = “userPhoneList”) | 方法 | 标识这是一个字段解析器(类似于Resolver)。parentType指定父类型,field指定是父类型的某个字段 |
@DgsDataLoader(name = “userPhoneList”) | 类 | 标识这个是一个属性获取的类(类似于Loader),通过name字段指定获取的字段名 |
@InputArgument | 参数 | 标识这个参数对应哪个入参 |
.graphql文件定义返回值->@DgsData为返回值进行填充->DgsDataLoader异步获取所需要的返回值。
我们在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 需要实现BatchLoader的原因)。
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-dgs文档:https://netflix.github.io/dgs/getting-started/