GraphQL系列 - 第2讲 Spring集成GraphQL

news2024/11/5 16:32:57

目录

    • 一、maven依赖
    • 二、Schema 定义
    • 三、代码集成
      • 3.1 创建模型类
      • 3.2 创建服务类
      • 3.3 创建控制器类
    • 四、单元测试
    • 五、实际 HTTP 请求测试
      • 5.1 查询单个 Person
      • 5.2 查询所有 People
      • 5.3 添加 Person
    • 六、其他
      • 6.1 开启graphiql
      • 6.2 开启schema查看端点

一、maven依赖

首先,在 pom.xml 文件中添加以下依赖:

<!-- Spring GraphQL -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>


<!-- Spring GraphQL Test -->
<dependency>
	<groupId>org.springframework.graphql</groupId>
	<artifactId>spring-graphql-test</artifactId>
	<scope>test</scope>
</dependency>
<!-- Unless already present in the compile scope -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-webflux</artifactId>
	<scope>test</scope>
</dependency>

二、Schema 定义

src/main/resources/graphql 目录下创建 person.graphqls 文件,定义 GraphQL Schema:

type Query {
    person(id: ID!): Person
    people: [Person]
}

type Mutation {
    addPerson(input: PersonInput!): Person
}

type Person {
    id: ID!
    name: String!
    age: Int
    dept: Dept
}

type Dept {
    id: ID!
    code: String!
    name: String!
}

input PersonInput {
    name: String!
    age: Int
    deptId: String
}


三、代码集成

3.1 创建模型类

PersonDeptPersonInput 模型类:

import lombok.Data;

@Data
public class Person {
    private String id;
    private String name;
    private int age;
    private Dept dept;
}

@Data
public class Dept {
    private String id;
    private String code;
    private String name;
}

@Data
public class PersonInput {
    private String name;
    private int age;
    private String deptId;
}

3.2 创建服务类

PersonService 服务类:

注:如下服务类可根据需求调整,本实现仅作为示例作用,实际开发时可通过数据库存储、调用其他服务等来生成相应的底层数据。

import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Service
public class PersonService {

    private List<Person> people = new ArrayList<>();

    public Person getPersonById(String id) {
        return people.stream().filter(person -> person.getId().equals(id)).findFirst().orElse(null);
    }

    public Dept getDeptByPersonId(String id) {
        Dept dept = new Dept();
        dept.setId("dept_001");
        dept.setCode("001");
        dept.setName("dept001");
        return dept;
    }

    public List<Person> getAllPeople() {
        return people;
    }

    public Person addPerson(PersonInput input) {
        Person person = new Person();
        person.setId(UUID.randomUUID().toString());
        person.setName(input.getName());
        person.setAge(input.getAge());
        people.add(person);
        return person;
    }
}

3.3 创建控制器类

PersonController 控制器类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;

import java.util.List;

@Controller
public class PersonController {

    @Autowired
    private PersonService personService;

    @QueryMapping
    public Person person(@Argument String id) {
        return personService.getPersonById(id);
    }

    @SchemaMapping
    public Dept dept(Person person) {
        return personService.getDeptByPersonId(person.getId());
    }

    @QueryMapping
    public List<Person> people() {
        return personService.getAllPeople();
    }

    @MutationMapping
    public Person addPerson(@Argument PersonInput input) {
        return personService.addPerson(input);
    }
}

关于映射注解的说明可参见下表:

注解用途作用
@QueryMapping映射 GraphQL 查询操作将 GraphQL 查询请求映射到控制器中的方法
@MutationMapping映射 GraphQL 变更操作将 GraphQL 变更请求(如创建、更新、删除操作)映射到控制器中的方法
@SchemaMapping映射 GraphQL 模式中的字段将 GraphQL 模式中的字段映射到控制器中的方法,通常用于嵌套对象的解析

重点解释下@SchemaMapping注解,该注解将 GraphQL 模式中的字段映射到控制器中的方法,通常用于嵌套对象的解析,包括属性如下:

  • typeName:指定 GraphQL 类型的名称。默认情况下,Spring 会根据 方法参数类型 自动推断。
  • field:指定 GraphQL 字段的名称。默认情况下,Spring 会根据 方法名称 自动推断。

示例代码

# schema.graphqls
type Person {
    id: ID!
    name: String!
    age: Int
    dept: Dept
}

type Dept {
    id: ID!
    code: String!
    name: String!
}
@SchemaMapping(typeName = "Person", field = "dept")
public Dept dept(Person person) {
    return personService.getDeptByPersonId(person.getId());
}
  • typeName:在上面的示例中,typeName 被设置为 "Person",表示这个方法是处理 Person 类型
  • fieldfield 被设置为 "dept",表示这个方法是处理 Person 类型中的 dept 字段
  • 如上 typeNamefield 属性可以省略,若省略则根据 方法参数类型 推断typeNamePerson,根据 方法名 推断fielddept

@QueryMapping@MutationMapping可以看作是特殊的@SchemaMapping,具体对应关系如下表:

注解说明适用范围
@SchemaMapping需要自定义typeName和field属性均适用,通常用于type类型(对象类型)中的field映射
@QueryMappingtypeName固定为Query,仅需自定义field属性适用于type Query { ... } 中方法映射
@MutationMappingtypeName固定为Mutation,仅需自定义field属性适用type Mutation { ... } 中方法映射

四、单元测试

PersonControllerTest 单元测试类:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.graphql.test.tester.GraphQlTester;

import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;

@GraphQlTest({PersonController.class})
public class PersonControllerTest {

    @MockBean
    private PersonService personService;

    @Autowired
    private GraphQlTester graphQlTester;

    @Test
    public void testPersonQuery_withQueryStr() {
        Person person = new Person();
        person.setId("1");
        person.setName("John Doe");
        person.setAge(30);
        when(personService.getPersonById("1")).thenReturn(person);
        //document对应查询文本
        graphQlTester.document("{ person(id: \"1\") { id name age } }")
                .execute()
                .path("person")
                .entity(Person.class)
                .satisfies(p -> {

                    assertEquals("1", p.getId());
                    assertEquals("John Doe", p.getName());
                    assertEquals(30, p.getAge());
                });
    }

    @Test
    public void testPersonQuerySimple() {
        Person person = new Person();
        person.setId("1");
        person.setName("John Doe");
        person.setAge(30);
        when(personService.getPersonById("1")).thenReturn(person);

        //documentName对应查询文本文件名(位于src/text/resources/graphql-test下)
        graphQlTester.documentName("person_simple")
                .execute()
                .path("person")
                .entity(Person.class)
                .satisfies(p -> {
                    assertEquals("1", p.getId());
                    assertEquals("John Doe", p.getName());
                    assertEquals(30, p.getAge());
                });
    }

    @Test
    public void testPersonQueryComplex() {
        Person person = new Person();
        person.setId("1");
        person.setName("John Doe");
        person.setAge(30);

        Dept dept = new Dept();
        dept.setId("dept_001");
        dept.setCode("001");
        dept.setName("dept001");

        when(personService.getPersonById("1")).thenReturn(person);
        when(personService.getDeptByPersonId("1")).thenReturn(dept);

        graphQlTester.documentName("person_complex")
                .execute()
                .path("person")
                .entity(Person.class)
                .satisfies(p -> {

                    assertEquals("1", p.getId());
                    assertEquals("John Doe", p.getName());
                    assertEquals(30, p.getAge());

                    Dept dept1 = p.getDept();
                    assertNotNull(dept1);
                    assertEquals("dept_001", dept1.getId());
                    assertEquals("001", dept1.getCode());
                    assertEquals("dept001", dept1.getName());
                });
    }

    @Test
    public void testPeopleQuery() {
        Person person1 = new Person();
        person1.setId("1");
        person1.setName("John Doe");
        person1.setAge(30);

        Person person2 = new Person();
        person2.setId("2");
        person2.setName("Jane Doe");
        person2.setAge(25);

        when(personService.getAllPeople()).thenReturn(Arrays.asList(person1, person2));

        graphQlTester.documentName("people")
                .execute()
                .path("people")
                .entityList(Person.class)
                .satisfies(people -> {
                    assertEquals(2, people.size());
                    assertEquals("1", people.get(0).getId());
                    assertEquals("John Doe", people.get(0).getName());
                    assertEquals(30, people.get(0).getAge());
                    assertEquals("2", people.get(1).getId());
                    assertEquals("Jane Doe", people.get(1).getName());
                    assertEquals(25, people.get(1).getAge());
                });
    }

    @Test
    public void testAddPersonMutation() {
        PersonInput input = new PersonInput();
        input.setName("John Doe");
        input.setAge(30);

        Person person = new Person();
        person.setId("1");
        person.setName("John Doe");
        person.setAge(30);

        when(personService.addPerson(input)).thenReturn(person);

        graphQlTester.documentName("addPerson")
                .execute()
                .path("addPerson")
                .entity(Person.class)
                .satisfies(p -> {
                    assertEquals("1", p.getId());
                    assertEquals("John Doe", p.getName());
                    assertEquals(30, p.getAge());
                });
    }
}

如上测试类中注意区分graphQLTester.documentgraphQLTester.documentName,其中document方法的参数为query字符串,而documentName对应query文件名(不包括文件类型,如people.graphql即对应people),文件内容即为具体的query字符串,dcoumentName参数中指定的文件位于src/test/resources/graphql-test目录下,如下图:
在这里插入图片描述

具体的query文件内容如下:

person_simple.graphql

{
    person(id: "1") {
        id
        name
        age
    }
}

person_complex.graphql

{
    person(id: "1") {
        id
        name
        age
        dept {
            id
            code
            name
        }
    }
}

people.graphql

{
    people {
        id
        name
        age
    }
}

addPerson.graphql

mutation {
    addPerson(input: { name: "John Doe", age: 30 }) {
        id
        name
        age
    }
}

五、实际 HTTP 请求测试

可以使用 Apifox、Postman 或 cURL 来测试 GraphQL API。

5.1 查询单个 Person

注:
/graphql查询请求的method为 POST
查询参数以JSON请求体进行传输,
且注意 双引号需要进行转义

在这里插入图片描述
具体请求参数:

{
  "query": "{ person(id: \"48afdb68-0dcb-457b-ba55-bb2750e35b82\") { id name age dept { id code name } } }"
}

亦可替换为(添加query前缀,后续query方法同):

{
  "query": "query { person(id: \"48afdb68-0dcb-457b-ba55-bb2750e35b82\") { id name age dept { id code name } } }"
}

5.2 查询所有 People

在这里插入图片描述
具体请求参数:

{
  "query": "{ people { id name age } }"
}

5.3 添加 Person

在这里插入图片描述

具体请求参数:

{
  "query": "mutation { addPerson(input: { name: \"Alice\", age: 28 }) { id name age dept { id code name } } }"
}

六、其他

6.1 开启graphiql

在application.yaml配置文件中开启graphiql相关配置:

spring:
  graphql:
    # 启用graphiql
    graphiql:
      enabled: true
      path: /graphiql

之后即可访问该/graphiql端点进行graphql调试:
在这里插入图片描述

6.2 开启schema查看端点

在application.yaml配置文件中开启schema查看端点相关配置:

spring:
  graphql:
    # schema相关配置
    schema:
      # 启用接口/graphql/schema - 查询schema定义
      printer:
        enabled: true

之后即可访问该/graphql/schema端点查看schema定义:
在这里插入图片描述


参考:
https://docs.spring.io/spring-boot/reference/web/spring-graphql.html
https://docs.spring.io/spring-boot/reference/testing/spring-boot-applications.html#testing.spring-boot-applications.spring-graphql-tests
https://docs.spring.io/spring-graphql/reference/controllers.html
https://github.com/spring-projects/spring-graphql/tree/1.0.x/samples/webmvc-http

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2231559.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何将字母l劈开

如何将字母l从顶到底劈开&#xff1f; 一、分两步&#xff0c;将字母 l 劈开 个人认为&#xff0c;将字母l劈开&#xff0c;需要做两件事情&#xff0c;或者说可以通过如下两个步骤来实现&#xff1a; 【1】证明字母 l 是一个象形字母&#xff1a;即字母l它的本质&#xff0…

linux驱动-输入子系统框架讲解

Input 子系统包括三个层次&#xff0c;分别是设备驱动层&#xff0c;核心层&#xff0c;事件处理层。 为什么要分层呢? 比如我们开发了一个驱动程序 a.c&#xff0c;其中里面有 100 行代码是通用的&#xff0c;然后我又开发了一个 驱动程序 b.c&#xff0c;那这 100 行…

【C/C++】字符/字符串函数(0)(补充)——由ctype.h提供

零.导言 除了字符分类函数&#xff0c;字符转换函数也是一类字符/字符串函数。 C语言提供了两种字符转换函数&#xff0c;分别是 toupper &#xff0c; tolower。 一.什么是字符转换函数&#xff1f; 顾名思义&#xff0c;即转换字符的函数&#xff0c;如大写字母转小写字母&am…

ssh和ssl的区别在哪些方面?

在网络安全和数据保护领域&#xff0c;谈话中经常提到的两个词是SSH(安全外壳)和SSL(安全套接字层)。尽管这两者在在线通信安全中都具有重要意义&#xff0c;但它们的使用目的不同&#xff0c;并且处于网络堆栈的不同级别。本文将深入分析 SSH 和 SSL 主要区别在哪些方面。 概念…

简单的ELK部署学习

简单的ELK部署学习 1. 需求 我们公司现在使用的是ELK日志跟踪&#xff0c;在出现问题的时候&#xff0c;我们可以快速定为到问题&#xff0c;并且可以对日志进行分类检索&#xff0c;比如对服务名称&#xff0c;ip , 级别等信息进行分类检索。此文章为本人学习了解我们公司的…

2024年11月3号深铁璟城人才房看房记

我为什么看深铁璟城二期? 答&#xff1a;价格哈。 最开始看大康书记的文章是预测2.88万&#xff0c;由于个人经济缘故保障房超过2.5w就不去看房。没想到周五的时候&#xff0c;人才房群里销售精英说均价为2.4~2.5w了&#xff0c;这不就是和润珑苑的定价策略接近嘛&#xff1f;…

零基础Java第十二期:类和对象(三)

目录 一、static成员&#xff08;补&#xff09; 1.1. static修饰成员方法 1.2. static成员变量初始化 二、代码块 2.1. 静态代码块和实例代码块 ​三、对象的打印 一、static成员&#xff08;补&#xff09; 1.1. static修饰成员方法 public class Linear {public st…

钉钉平台开发小程序

一、下载小程序开发者工具 官网地址&#xff1a;小程序开发工具 - 钉钉开放平台 客户端类型 下载链接 MacOS x64 https://ur.alipay.com/volans-demo_MiniProgramStudio-x64.dmg MacOS arm64 https://ur.alipay.com/volans-demo_MiniProgramStudio-arm64.dmg Windows ht…

本地部署bert-base-chinese模型交互式问答,gradio

首先下载bert-base-chinese&#xff0c;可以在 Huggingface, modelscope, github下载 pip install gradio torch transformers import gradio as gr import torch from transformers import BertTokenizer, BertForQuestionAnswering# 加载bert-base-chinese模型和分词器 mod…

正式开源:从 Greenplum 到 Cloudberry 迁移工具 cbcopy 发布

Cloudberry Database 作为 Greenplum 衍生版本和首选开源替代&#xff0c;由 Greenplum 原始团队成员创建&#xff0c;与 Greenplum 保持原生兼容&#xff0c;并能实现无缝迁移&#xff0c;且具备更新的 PostgreSQL 内核和更丰富的功能。GitHub: https://github.com/cloudberry…

计算机网络:网络层 —— 路由信息协议 RIP

文章目录 路由选择协议动态路由协议路由信息协议 RIPRIP 的重要特点RIP的基本工作过程RIP的距离向量算法RIP存在的问题RIP版本和相关报文的封装 路由选择协议 因特网是全球最大的互联网&#xff0c;它所采取的路由选择协议具有以下三个主要特点&#xff1a; 自适应&#xff1a…

构建主干交换网络实验

转载请注明出处 该实验为交换网络综合实验&#xff0c;仅供参考。 根据下表配置计算机IP地址和划分VLAN。划分方式见课本48页&#xff0c;不再赘述 计算机名 IP地址 所属VLAN PC0 192.168.10.1 VLAN10 PC1 192.168.20.1 VLAN20 PC2 192.168.10.2 VLAN10 PC3 192.…

MySQL超大分页怎么优化处理?limit 1000000,10 和 limit 10区别?覆盖索引、面试题

1. limit 100000,10 和 limit 10区别 LIMIT 100000, 10&#xff1a; 这个语句的意思是&#xff0c;从查询结果中跳过前100000条记录&#xff0c;然后返回接下来的10条记录。这通常用于分页查询中&#xff0c;当你需要跳过大量的记录以获取后续的记录时。例如&#xff0c;如果你…

电科金仓(人大金仓)更新授权文件(致命错误: XX000: License file expired.)

问题:电科金仓(人大金仓)数据库链接异常,重启失败,查看日志如下: 致命错误: XX000: License file expired. 位置: PostmasterMain, postmaster.c:725 解决方法: 一、下载授权文件 根据安装版本在官网下载授权文件(电科金仓-成为世界卓越的数据库产品与服务提供商)…

DMFLDR数据载入使用实践

1、DMFLDR概述 1.1DMFLDR功能介绍 dmfldr&#xff08;DM Fast Loader&#xff09;是 DM 提供的快速数据装载命令行工具。用户通过使用 dmfldr 工具能够把按照一定格式 排序的文本数据以简单、快速、高效的方式载入到 DM 数据库中&#xff0c;或把 DM 数据库中的数据按照一定格…

计算机网络(Ⅵ)应用层原理

一些网络应用的例子: E-mail Internaet电话 Web 电子支付 文本信息 搜索 P2P文件共享 流媒体 即时通讯 实时电视会议 .... .... 创建一个网络应用&#xff1a; 1.编程 2.在不同的端系统上运行。 网络应用的体系架构 可能的应用架构 1.客户-服…

《使用Gin框架构建分布式应用》阅读笔记:p393-p437

《用Gin框架构建分布式应用》学习第17天&#xff0c;p393-p437总结&#xff0c;总45页。 一、技术总结 1.Prometheus Prometheus放在代码里面使用&#xff0c;还是第一次见。在本人实际的工作中未看到这种用法。 2.Grafana Grafana用于被监控数据的可视化。 3.Telegraf …

【动手学强化学习】part7-Actor-Critic算法

阐述、总结【动手学强化学习】章节内容的学习情况&#xff0c;复现并理解代码。 文章目录 一、算法背景1.1 算法目标1.2 存在问题1.3 解决方法 二、Actor-Critic算法2.1 必要说明 优势函数 2.2 伪代码 算法流程简述 2.3 算法代码2.4 运行结果 结果分析 2.5 算法流程说明 初始化…

MySQL【二】

查询列 SELECT [ALL | DISTINCT ] * | 列名1[,……列名n] FROM 表名; 查询所有选课学生的学号&#xff0c;结果去除重复值 select distinct sno from sc; 选择行 查询满足条件的数据集 SELECT 字段列表 FROM 表名 WHERE 查询条件 查询不属于数学系或外国语系的学生全部信息 …

ElasticSearch - Bucket Selector使用指南

文章目录 官方文档Bucket Selector1. 定义2. 工作原理3. 使用场景与示例使用场景官方案例示例2 4. 注意事项5. 总结 官方文档 https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html Bucket Selector https://www.elastic.co/guide/en/…