GraphQL & DGS-Framework入门

323 阅读5分钟

GraphQL

Graphql 官方文档

入门

GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

一个 GraphQL 服务是通过定义类型和类型上的字段来创建的,然后给每个类型上的每个字段提供解析函数。例如,一个 GraphQL 服务告诉我们当前登录用户是 me,这个用户的名称可能像这样:

type Query {
  me: User
}
 
type User {
  id: ID
  name: String
}

一旦一个 GraphQL 服务运行起来(通常在 web 服务的一个 URL 上),它就能接收 GraphQL 查询,并验证和执行。接收到的查询首先会被检查确保它只引用了已定义的类型和字段,然后运行指定的解析函数来生成结果。

例如这个查询:

{
    me {
        name
    }
}

会产生这样的JSON结果:

{
    "me": {
        "name": "Luke Skywalker"
    }
}

字段

在前一例子中,我们请求了我们主角的名字,返回了一个字符串类型(String),但是字段也能指代对象类型(Object)。这个时候,你可以对这个对象的字段进行次级选择(sub-selection)。GraphQL 查询能够遍历相关对象及其字段,使得客户端可以一次请求查询大量相关数据,而不像传统 REST 架构中那样需要多次往返查询。

{
  hero {
    name
    # 查询可以有备注!
    friends {
      name
    }
  }
}
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

参数

{
  human(id: "1000") {
    name
    height
  }
}
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 1.72
    }
  }
}

别名

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

片段

GraphQL 包含了称作片段的可复用单元。片段使你能够组织一组字段,然后在需要它们的地方引入。

{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}

变量

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

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

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

指令

image.png

内联片段

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

image.png

这个查询中,hero 字段返回 Character 类型,取决于 episode 参数,其可能是 Human 或者 Droid 类型。在直接选择的情况下,你只能请求 Character 上存在的字段,譬如 name。

如果要请求具体类型上的字段,你需要使用一个类型条件内联片段。因为第一个片段标注为 ... on Droid,primaryFunction 仅在 hero 返回的 Character 为 Droid 类型时才会执行。同理适用于 Human 类型的 height 字段。

DGS-Framework

Spring Boot接入Graphql有两种比较好的选择

  • Netflix DGS-Framework
  • Spring-Graphql

DGS的优势

  • github star更多
  • 出的时间比Spring-Graphql早

Spring-Graphql的优势

  • 使用原生Java编写(DGS主要用Kotlin)
  • Spring官方推出

因为公司使用的是DGS,我也使用DGS

目前只使用graphql做查询操作

DGS官方文档

环境:JDK21,MySQL8,spring boot3

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
<groupId>com.netflix.graphql.dgs</groupId>
<artifactId>graphql-dgs-spring-graphql-starter</artifactId>
<version>8.5.6</version>
</dependency>
<dependency>
    <groupId>com.netflix.graphql.dgs</groupId>
    <artifactId>graphql-dgs-spring-graphql-starter-test</artifactId>
    <version>8.5.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.3.0</version>
    <scope>runtime</scope>
</dependency>
<!-- mybatis-plus-boot-starter -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.5</version>
</dependency>

接入MySQL

模拟blog网站,定义user表和article表

create table user(
    id bigint unsigned primary key auto_increment,
    name varchar(300),
    age int unsigned,
    email varchar(50)
);

create table article(
    id bigint unsigned primary key auto_increment,
    title varchar(50),
    content text,
    user_id bigint unsigned
);

schema.graphqls

type Query {
    getUser:[User]
    getUserById(id:ID!):User
    getUserPage(page:Int!,size:Int!):[User]
    getArticleByUserAge(age:Int!):[Article]
}

type User {
    id:ID!
    name:String
    age:Int
    email:String
    articles:[Article]
}

type Article{
    id:ID!
    title:String
    content:String
    userId:Int!
}

Java

@DgsComponent
@RequiredArgsConstructor
public class UserDataFetcher {

    private final UserMapper userMapper;
    private final ArticleMapper articleMapper;

    @DgsQuery
    public Collection<User> getUser() {
        return userMapper.selectList(null);
    }

    @DgsQuery
    public User getUserById(@InputArgument String id) {
        return userMapper.selectById(Long.parseLong(id));
    }
    
    @DgsQuery
    public List<User> getUserPage(@InputArgument Integer page, @InputArgument Integer size) {
        return userMapper.selectPage(new Page(page, size), null).getRecords();
    }

    @DgsQuery
    public List<Article> getArticleByUserAge(@InputArgument Integer age) {
        List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getAge, age));
        List<Article> articles = new ArrayList<>();
        for (User user : users) {
            articles.addAll(articleMapper.selectList(new LambdaQueryWrapper<Article>().eq(Article::getUserId, user.getId())));
        }
        return articles;
    }
}

我们可以发现graphql定义的User比MySQL多一个List<Article>字段,我们查的时候如何查到呢?

懒加载

我们可以通过懒加载查询,当query没有article字段的时候就不查Article表

@DgsData(parentType = "User")
public Collection<Article> articles(@NotNull DgsDataFetchingEnvironment env){
    User user = env.getSource();
    return articleMapper.selectList(new LambdaQueryWrapper<Article>().eq(Article::getUserId, user.getId()));
}

但是这又带来了1+N问题

1+N

什么是1+N问题?

一次SQL查询后又要N次SQL查询才能结束此次查询就是1+N问题

在getUser时会有N个User,此时又要通过N次查询才能查出对应的Article

我们可以使用@DgsDataLoaderMappedBatchLoader解决

@DgsDataLoader(name = "articles",caching = true)
@RequiredArgsConstructor
public class ArticleDataLoader implements MappedBatchLoader<Long, List<Article>> {

    private final ArticleMapper articleMapper;

    @Override
    public CompletionStage<Map<Long, List<Article>>> load(Set<Long> set) {
        return CompletableFuture.supplyAsync(()-> listArticleByUserIds(set));
    }

//    还是1+N
//    public Map<Long, List<Article>> listArticleByUserIds(Collection<Long> userIds) {
//        Map<Long, List<Article>> map = new HashMap<>();
//        for (Long userId : userIds) {
//            map.put(userId, articleMapper.selectList(new LambdaQueryWrapper<Article>().eq(Article::getUserId, userId)));
//        }
//        return map;
//    }

    public Map<Long, List<Article>> listArticleByUserIds(Collection<Long> userIds) {
        Map<Long, List<Article>> map = new HashMap<>();
        List<Article> articles = articleMapper.selectList(new LambdaQueryWrapper<Article>().in(Article::getUserId, userIds));
        for (Article article : articles) {
            map.compute(article.getUserId(),(k,v)->{
                if(v == null) {
                    v = new ArrayList<>();
                }
                v.add(article);
                return v;
            });
        }
        return map;
    }
}
@DgsData(parentType = "User")
public CompletableFuture<List<Article>> articles(@NotNull DgsDataFetchingEnvironment env) {
    User user = env.getSource();
    DataLoader<Long, List<Article>> articleDataLoader = env.getDataLoader("articles");
    return articleDataLoader.load(user.getId());
}

这样再调用getUser后只会有一个SQL查询所有需要的article