中文

通过模式拼接释放 GraphQL 联邦的强大功能。学习如何从多个服务构建统一的 GraphQL API,从而提高可扩展性和可维护性。

GraphQL 联邦:模式拼接 (Schema Stitching) - 全面指南

在不断发展的现代应用程序开发领域,对可扩展和可维护架构的需求已变得至关重要。微服务以其固有的模块化和独立可部署性,已成为一种流行的解决方案。然而,管理众多微服务可能会引入复杂性,尤其是在向客户端应用程序暴露统一 API 方面。这正是 GraphQL 联邦,特别是模式拼接 (Schema Stitching) 发挥作用的地方。

什么是 GraphQL 联邦?

GraphQL 联邦是一种强大的架构,允许您从多个底层的 GraphQL 服务(通常代表微服务)构建一个单一、统一的 GraphQL API。它使开发人员能够跨不同服务查询数据,就好像它们是一个单一的图谱,从而简化了客户端体验,并减少了客户端进行复杂编排逻辑的需求。

实现 GraphQL 联邦主要有两种方法:

本文将重点介绍 模式拼接 (Schema Stitching),探讨其概念、优点、局限性和实际实现。

理解模式拼接

模式拼接是将多个 GraphQL 模式合并为一个单一、内聚的模式的过程。这个统一的模式充当一个外观(facade),向客户端隐藏了底层服务的复杂性。当客户端向拼接后的模式发出请求时,网关会智能地将请求路由到适当的底层服务,检索数据,并在将结果返回给客户端之前将其组合起来。

可以这样理解:您有多家专门从事不同菜系的餐厅(服务)。模式拼接就像一个万能菜单,它结合了每家餐厅的所有菜肴。当顾客(客户端)从这个万能菜单点餐时,订单被智能地路由到相应的餐厅厨房,食物被准备好,然后合并成一次单独的配送交付给顾客。

模式拼接中的关键概念

模式拼接的优势

对于采用微服务架构的组织来说,模式拼接提供了几个引人注目的优势:

模式拼接的局限性

虽然模式拼接提供了许多优点,但了解其局限性也很重要:

模式拼接的实践

让我们通过一个简化的例子,看看如何使用 Node.js 和 graphql-tools 库(一个流行的模式拼接选择)来实现模式拼接。这个例子涉及两个微服务:一个用户服务和一个产品服务

1. 定义远程模式

首先,为每个远程服务定义 GraphQL 模式。

用户服务 (user-service.js):


const { buildSchema } = require('graphql');

const userSchema = buildSchema(`
  type User {
    id: ID!
    name: String
    email: String
  }

  type Query {
    user(id: ID!): User
  }
`);

const users = [
  { id: '1', name: 'Alice Smith', email: 'alice@example.com' },
  { id: '2', name: 'Bob Johnson', email: 'bob@example.com' },
];

const userRoot = {
  user: (args) => users.find(user => user.id === args.id),
};

module.exports = {
  schema: userSchema,
  rootValue: userRoot,
};

产品服务 (product-service.js):


const { buildSchema } = require('graphql');

const productSchema = buildSchema(`
  type Product {
    id: ID!
    name: String
    price: Float
    userId: ID!  # Foreign key to User Service
  }

  type Query {
    product(id: ID!): Product
  }
`);

const products = [
  { id: '101', name: 'Laptop', price: 1200, userId: '1' },
  { id: '102', name: 'Smartphone', price: 800, userId: '2' },
];

const productRoot = {
  product: (args) => products.find(product => product.id === args.id),
};

module.exports = {
  schema: productSchema,
  rootValue: productRoot,
};

2. 创建网关服务

现在,创建将两个模式拼接在一起的网关服务。

网关服务 (gateway.js):


const { stitchSchemas } = require('@graphql-tools/stitch');
const { makeRemoteExecutableSchema } = require('@graphql-tools/wrap');
const { graphqlHTTP } = require('express-graphql');
const express = require('express');
const { introspectSchema } = require('@graphql-tools/wrap');
const { printSchema } = require('graphql');
const fetch = require('node-fetch');

async function createRemoteSchema(uri) {
  const fetcher = async (params) => {
    const response = await fetch(uri, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(params),
    });
    return response.json();
  };

  const schema = await introspectSchema(fetcher);
  return makeRemoteExecutableSchema({
    schema,
    fetcher,
  });
}

async function main() {
  const userSchema = await createRemoteSchema('http://localhost:4001/graphql');
  const productSchema = await createRemoteSchema('http://localhost:4002/graphql');

  const stitchedSchema = stitchSchemas({
    subschemas: [
      { schema: userSchema },
      { schema: productSchema },
    ],
    typeDefs: `
      extend type Product {
        user: User
      }
    `,
    resolvers: {
      Product: {
        user: {
          selectionSet: `{ userId }`,
          resolve(product, args, context, info) {
            return info.mergeInfo.delegateToSchema({
              schema: userSchema,
              operation: 'query',
              fieldName: 'user',
              args: {
                id: product.userId,
              },
              context,
              info,
            });
          },
        },
      },
    },
  });

  const app = express();
  app.use('/graphql', graphqlHTTP({
    schema: stitchedSchema,
    graphiql: true,
  }));

  app.listen(4000, () => console.log('网关服务器运行在 http://localhost:4000/graphql'));
}

main().catch(console.error);

3. 运行服务

您需要在不同的端口上运行用户服务和产品服务。例如:

用户服务 (端口 4001):


const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { schema, rootValue } = require('./user-service');

const app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: rootValue,
  graphiql: true,
}));

app.listen(4001, () => console.log('用户服务运行在 http://localhost:4001/graphql'));

产品服务 (端口 4002):


const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { schema, rootValue } = require('./product-service');

const app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: rootValue,
  graphiql: true,
}));

app.listen(4002, () => console.log('产品服务运行在 http://localhost:4002/graphql'));

4. 查询拼接后的模式

现在您可以通过网关(运行在 4000 端口)查询拼接后的模式。您可以运行如下查询:


query {
  product(id: "101") {
    id
    name
    price
    user {
      id
      name
      email
    }
  }
}

此查询检索 ID 为 “101” 的产品,并从用户服务中获取关联的用户,展示了模式拼接如何让您在单个请求中跨多个服务查询数据。

高级模式拼接技术

除了基本示例外,这里还有一些可以用来增强模式拼接实现的高级技术:

在模式拼接和 Apollo 联邦之间进行选择

虽然模式拼接是 GraphQL 联邦的一个可行选项,但由于其更高级的功能和改进的开发人员体验,Apollo 联邦已成为更受欢迎的选择。以下是两种方法的比较:

特性 模式拼接 Apollo 联邦
模式定义 使用现有的 GraphQL 模式语言 使用带指令的声明式模式语言
查询规划 需要手动进行查询委托 由 Apollo Gateway 自动进行查询规划
类型扩展 有限的支持 内置对类型扩展的支持
键指令 不支持 使用 @key 指令识别实体
分布式追踪 需要手动实现 内置对分布式追踪的支持
工具和生态系统 工具不太成熟 更成熟的工具和庞大的社区
复杂性 在大型系统中管理可能很复杂 为大型复杂系统设计

何时选择模式拼接:

何时选择 Apollo 联邦:

真实世界示例和用例

以下是一些 GraphQL 联邦(包括模式拼接)的真实世界应用示例:

模式拼接的最佳实践

为确保成功实施模式拼接,请遵循以下最佳实践:

结论

通过模式拼接实现的 GraphQL 联邦为在微服务架构中从多个服务构建统一 API 提供了一种强大的方法。通过理解其核心概念、优点、局限性和实现技术,您可以利用模式拼接来简化数据访问、提高可扩展性并增强可维护性。虽然 Apollo 联邦已成为一种更先进的解决方案,但对于更简单的场景或在集成现有 GraphQL 服务时,模式拼接仍然是一个可行的选择。仔细考虑您的具体需求和要求,为您的组织选择最佳方法。