網絡傳輸架構之GraphQL講解

1 GraphQL架構風格

1.1 簡介

有些小伙伴在工作中可能遇到過這樣的場景:移動端只需要用戶的姓名和郵箱,但REST API返回了用戶的所有信息,造成數據傳輸浪費。

GraphQL正是為了解決這個問題而生的。

GraphQL包含三個核心組件:

  • Schema定義:強類型系統(tǒng)描述API能力
  • 查詢語言:客戶端精確請求需要的數據
  • 執(zhí)行引擎:解析查詢并返回結果

GraphQL的優(yōu)缺點分析

  • 優(yōu)點:
    精確的數據獲取,避免過度獲取
    單一端點,減少HTTP連接開銷
    強類型系統(tǒng),自動生成文檔
    前端主導數據需求
  • 缺點:
    查詢復雜度控制困難
    緩存實現復雜(HTTP緩存失效)
    N+1查詢問題需要額外處理
    學習曲線相對陡峭

1.2 GraphQL執(zhí)行流程

image.png

1.3 相關語法

GraphQL 操作語法其實只有 3 種頂層類型:Query、MutationSubscription,但寫進紙面的“語法單元”有 10 來種。
可到此處進行驗證語法:https://countries.trevorblades.com/

3 種根操作(Operation):

  • Query:只讀
  • Mutation:寫操作
  • Subscription:實時推送(基于 WebSocket)

語法模板,方括號代表可省

operationType [operationName] ( [variableDefinitions] ) {
  selectionSet
}

1.3.1 Query(查詢)

最簡單字段列表

{
  user(id: 4) {
    name
    avatar
  }
}

起個操作名(方便調試/日志)

query GetUser {
  user(id: 4) {
    name
  }
}

傳變量(推薦,避免字符串拼接)

query GetUser($uid: ID!) {   # 變量定義
  user(id: $uid) {
    name
    posts {                  # 嵌套對象
      title
      comments(first: 3) {   # 分頁實參
        body
      }
    }
  }
}

此處ID表示是類型,加感嘆號!區(qū)別:

寫法 含義 舉例
ID 可以是 null 的 ID "user-123"null
ID! 絕對不能為 null 的 ID 必須傳 "user-123"傳 null 或干脆不傳都會報錯

對于$使用變量聲明時才用

位置 寫法 角色
形參列表 ($uid: ID!) 變量聲明——告訴 GraphQL“等會兒外部會傳個變量叫 uid”
查詢體內 user(id: $uid) 變量使用——把剛才聲明的那個變量插進來

別名(同一字段查兩次)

{
  smallPic: user(id: 4) { avatar(size: 64)  }
  bigPic:   user(id: 4) { avatar(size: 512) }
}

Fragment(復用片段)

//必須指向 on User 表示 這套字段只能展開在 User 對象上
fragment AvatarInfo on User {
  name
  avatar
}

query {
  u1: user(id: 4) { ...AvatarInfo }
  u2: user(id: 5) { ...AvatarInfo }
}

內聯片段(接口/聯合類型),... on Type { fields }inline fragment(內聯片段),按類型挑選字段

{
  search(keyword: "hero") {
  // GraphQL 內置字段,任何對象里都能拿,返回當前對象的真實類型名(字符串)
    __typename 
    ... on Movie { title }
    ... on Book  { author }
  }
}

1.3.2 Mutation(變更)

創(chuàng)建 + 返回結果

//mutation  聲明“我要改”  CreatePost 本次操作的命名
mutation CreatePost($input: PostInput!) {
  // schema 里定義的 mutation 字段
  createPost(input: $input) { 
    // 要返回什么字段
    id
    title
    author { name }
  }
}

多個寫操作順序執(zhí)行(GraphQL 保證串行)

mutation {
  addComment(postId: 1, body: "nice") { id }
  updatePost(id: 1, views: +1) { views }
}

1.3.3 Subscription(訂閱)

語法與 Query 相同,但由服務器推回

subscription MessageAdded($room: ID!) {
  messageAdded(roomId: $room) {
    from { name }
    content
    createdAt
  }
}

客戶端通過 WebSocket 發(fā)送該幀,服務器在有人發(fā)言時回相同結構數據。

1.3.4 變量系統(tǒng)(Variable)

聲明:$var: Type = defaultValue,類型后加 ! 代表非空。
List / InputObject

query($ids: [ID!]!) {
  users(ids: $ids) { name }
}

1.3.5 指令(Directive)

@skip(if: Boolean):如果為 true,就跳過這塊,@include(if: Boolean):只有為 true 才保留這塊,同時參數 if 必須是 Boolean!(非空布爾),變量、字面量都可以

query GetUser($skipAvatar: Boolean!) {
  user(id: 4) {
    name
    avatar @skip(if: $skipAvatar)
  }
}

1.3.6 Introspection(自省)元字段

Meta-field(元字段),帶 __ 的字段不會出現在要寫的 schema 里,但任何對象都能查詢它們:

  • __typename:運行期真實類型,內置字段,任何對象里都能拿
  • __schema:整個 schema 的“根目錄”,掛在 最頂層的 Query 隱式字段
  • __type(name: "User"): 按名取類型詳情,同樣掛在頂層,用來查詢某一個具體類型 的字段、枚舉值、可能的接口實現等
{
  __schema {
    types {
      name
      kind
      fields { name type { name } }
    }
  }
}

1.3.7 錯誤與響應格式

GraphQL 總是 HTTP 200,錯誤放在 errors 數組:

{
  "data": { "user": null },
  "errors": [{
    "message": "User not found",
    "path": ["user"],
    "extensions": { "code": "USER_404" }
  }]
}

1.4 服務端操作

1.4.1 配置

1.4.1.1 pom.xml

springboot 2.x 主要是 接口方式 比如 QueryResolver

<dependency>
  <groupId>com.graphql-java-kickstart</groupId>
   <artifactId>graphql-spring-boot-starter</artifactId>
   <version>11.1.0</version>
</dependency>
<!-- 測試客戶端:GraphiQL(瀏覽器調試工具) -->
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>11.1.0</version>
</dependency>

spring 3.x 自帶graphql 主要使用注解方式@SchemaMapping、@QueryMapping 注解方式而不是 QueryResolver

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>

1.4.1.2 yml配置

graphql:
  servlet:
    # graphql 訪問地址
    mapping: /graphql
    auto-register: true
  tools:
    # graphql 一般存放resource 下的 graphql 文件夾以 .graphqls 結尾
    schema-location-pattern: graphql/**/schema*.graphqls

# 瀏覽器內使用 GraphiQL 啟用(默認 true)
graphiql:
  enabled: true
  mapping: /graphiql

1.4.1.1 Schema

type Query { //服務端定義而非客戶端的 query 定義
  userById(id: ID!): User
  users: [User]
}

type User {
  id: ID
  userName: String
  phoneNumer: String
  remark: String
}


type Mutation {
  addUser(userName: String!, phoneNumer: String!): Boolean  # 添加
}

1.4.2 業(yè)務操作

//查詢類
@Component
public class UserQuery implements GraphQLQueryResolver {

    @Autowired
    private UserService userService;

    public List<UserEntity> users() {
        return userService.list();
    }

    public UserEntity userById(String id) {
        return userService.getById(id);
    }
}

//新增類
@Component
public class UserMutation implements GraphQLMutationResolver {
    @Autowired
    private UserService userService;


    public Boolean addUser(String userName, String phoneNumer) {
        UserEntity book = new UserEntity();
        book.setUserName(userName);
        book.setPhoneNumer(phoneNumer);
        return userService.save(book);
    }
}

1.4.3 測試示例

查詢graphql schema定義:

query userList{
  users{
    id
    userName
    phoneNumer
    remark
  }
}

結果:
{
  "data": {
    "users": [
      {
        "id": "1",
        "userName": "哈哈",
        "phoneNumer": "1234556789",
        "remark": "1"
      }
    ]
  }
}

新增類示例:

mutation addUser{
  addUser(userName: "小明", phoneNumer: "123456879465")
}
結果:
{
  "data": {
    "addUser": true
  }
}

1.4.4 原生操作

1.4.4.1 配置

pom.xml

<dependency>
   <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java</artifactId>
    <version>24.3</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

schema如下

type Query {
  userById(id: ID!): User
  users: [User]
}
type User {
  id: ID
  userName: String
  phoneNumer: String
  remark: String
}
type Mutation {
  addUser(userName: String!, phoneNumer: String!): Boolean  # 添加
}

1.4.4.2 解析配置

@Configuration
public class GraphQLProvider {
    private GraphQL graphQL;

    @Autowired
    private UserService userService;   // 你的業(yè)務層

    @Bean
    public GraphQL graphQL() {
        return graphQL;
    }

    @PostConstruct
    public void init() throws IOException {
        // 1. 自動加載 resources/graphql 目錄下所有 .graphqls 文件
        SchemaParser schemaParser = new SchemaParser();
        TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry();

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources("classpath:graphql/**/*.graphqls");

        for (Resource resource : resources) {
            String sdl = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
            typeRegistry.merge(schemaParser.parse(sdl));
        }

        // 2. 綁定 DataFetcher(這就是替代 GraphQLQueryResolver 的方式)
        RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
                .type("Query", builder -> builder
                        .dataFetcher("users", env -> userService.list())
                        .dataFetcher("userById", env -> {
                            String id = env.getArgument("id");
                            return userService.getById(id);
                        })
                )
                .type("Mutation", builder -> builder
                        .dataFetcher("addUser", env -> {
                            String userName = env.getArgument("userName");
                            String phoneNumer = env.getArgument("phoneNumer");
                            UserEntity user = new UserEntity();
                            user.setUserName(userName);
                            user.setPhoneNumer(phoneNumer);
                            return userService.save(user);
                        })
                )
                .build();

        // 3. 生成可執(zhí)行 schema
        SchemaGenerator schemaGenerator = new SchemaGenerator();
        GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(
                SchemaGenerator.Options.defaultOptions(), // 可自定義嚴格校驗等
                typeRegistry,
                runtimeWiring
        );

        // 4. 創(chuàng)建 GraphQL 實例
        this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
    }

}

1.4.4.3 注冊servlet

@WebServlet(urlPatterns = "/graphql")
public class GraphQLServletConfig extends HttpServlet {

    private final GraphQL graphQL;  // 從 GraphQLProvider 注入,或靜態(tài)持有

    public GraphQLServletConfig(GraphQL graphQL) {
        this.graphQL = graphQL;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 1. 解析請求 body(GraphQL query JSON)
        String requestBody = req.getReader().lines().collect(Collectors.joining());
        Map<String, Object> input = new ObjectMapper().readValue(requestBody, Map.class);
        String query = (String) input.get("query");
        Map<String, Object> variables = (Map<String, Object>) input.getOrDefault("variables", Collections.emptyMap());

        // 2. 執(zhí)行 GraphQL
        ExecutionInput executionInput = ExecutionInput.newExecutionInput()
                .query(query)
                .variables(variables)
                .build();
        ExecutionResult executionResult = graphQL.execute(executionInput);

        // 3. 返回 JSON 響應
        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");
        resp.getWriter().write(new ObjectMapper().writeValueAsString(executionResult.toSpecification()));
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 支持 GET(可選,GraphQL 規(guī)范允許)
        doPost(req, resp);
    }
}

注意:啟動類別忘了@ServletComponentScan

1.4.4.4 測試

用bruno 模擬測試

查詢示例
query {
  users {
    id
    userName
    phoneNumer
    remark
  }
}

修改示例
mutation {
  addUser(
    userName:"小王"
    phoneNumer:"987654321"
  )  
}
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容