说明

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://s2.51cto.com/images/blog/202210/13233817_634830e936def64424.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)

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

操作名称

上面我们主要是查询,省略了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)。其工作方式跟类型语言中函数的参数定义一样。它以列出所有变量,<font color='red'>变量前缀必须为 $</font>,后跟其类型,本例中为 AddressInput。

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

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

默认变量(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和查询结果

GraphQL-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 <br> graphql中文官网