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
}
}
变量
使用变量之前,我们得做三件事:
- 使用
$variableName替代查询中的静态值。 - 声明
$variableName为查询接受的变量之一。 - 将
variableName: value通过传输专用(通常是 JSON)的分离的变量字典中。
默认变量
可以通过在查询中的类型定义后面附带默认值的方式,将默认值赋给变量。
指令
内联片段
如果你查询的字段返回的是接口或者联合类型,那么你可能需要使用内联片段来取出下层具体类型的数据:
这个查询中,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做查询操作
环境: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
我们可以使用@DgsDataLoader和MappedBatchLoader解决
@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