## 联合服务
[<span style="color:red">Apollo 联合服务</span>](https://www.apollographql.com/docs/federation/)提供了一种将单体式 GraphQL 服务器拆分为独立微服务的手段。它有两个组成部分:一个网关和一或多个联合微服务。每个微服务都持有部分 schema,网关将这些 schema 合并为一个可以被客户端使用的 schema。
引用[<span style="color:red">Apollo 文档</span>](https://www.apollographql.com/blog/announcement/apollo-federation-f260cf525d21/),联合服务的设计遵循以下核心原则:
- 构建图表应该是**声明式**的。使用联合服务,你可以在 schema 内部声明式地组合图表,而不是编写命令式 schema 拼接代码。
- 代码应该按**关注点**分割,而不是按类型。通常没有一个团队能控制像 User 或 Product 这种重要类型的各个方面,因此这些类型的定义应该分布在团队和代码库中,而不是写在一起。
- 图表应尽可能简单,以让客户端使用。同时,联合服务可以形成一个完整的、以产品为中心的图表,准确地反映它在客户端的使用情况。
- 它只是 GraphQL,仅使用符合规范的语言特性。任何语言,不仅仅是 JavaScript,都可以实现联合服务。
> Apollo 联合服务到目前为止还不支持订阅。
在接下来的例子中,我们将设置一个带有网关和两个联合端点的演示程序:一个 Users 服务和一个 Posts 服务,
### 联合示例:Users
首先,安装联合服务的依赖包:
```bash
npm install --save @apollo/federation @apollo/subgraph
```
### 模式优先
Users 服务有一个简单的 schema。注意 `@key` 这个指令:它告诉 Apollo 查询规划器,如果你有它的 `id`,则可以获取特定的 User 实例。另外,请注意我们也要继承这个 `Query` 类型。
```graphql
type User @key(fields: "id") {
id: ID!
name: String!
}
extend type Query {
getUser(id: ID!): User
}
```
我们的解析器有一个额外的方法:`resolveReference()`。每当相关资源需要 User 实例时,它就会被 Apollo 网关调用。我们在后面的 Posts 服务中也会看到这个例子。请注意 `@ResolveReference()` 这个装饰器。
```typescript
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { UsersService } from './users.service';
@Resolver('User')
export class UsersResolvers {
constructor(private usersService: UsersService) {}
@Query()
getUser(@Args('id') id: string) {
return this.usersService.findById(id);
}
@ResolveReference()
resolveReference(reference: { __typename: string; id: string }) {
return this.usersService.findById(reference.id);
}
}
```
最后,我们在模块中使用 `GraphQLFederationModule` 将所有东西连接起来。此模块接收与常规的 `GraphQLModule` 相同的配置。
```typescript
import { Module } from '@nestjs/common';
import { GraphQLFederationModule } from '@nestjs/graphql';
import { UsersResolvers } from './users.resolvers';
@Module({
imports: [
GraphQLFederationModule.forRoot({
typePaths: ['**/*.graphql'],
}),
],
providers: [UsersResolvers],
})
export class AppModule {}
```
### 代码优先
代码优先联合服务与常规的代码优先 GraphQL 很像。我们只需添加一些额外的装饰器到 `User` 实体即可。
```typescript
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
id: number;
@Field()
name: string;
}
```
我们的解析器有一个额外的方法:`resolveReference()`。每当相关资源需要 User 实例时,它就会被 Apollo 网关调用。我们在后面的 Posts 服务中也会看到这个例子。请注意 `@ResolveReference()` 这个装饰器。
```typescript
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsersResolvers {
constructor(private usersService: UsersService) {}
@Query((returns) => User)
getUser(@Args('id') id: number): User {
return this.usersService.findById(id);
}
@ResolveReference()
resolveReference(reference: { __typename: string; id: number }): User {
return this.usersService.findById(reference.id);
}
}
```
最后,我们在模块中使用 `GraphQLFederationModule` 将所有东西连接起来。此模块接收与常规的 `GraphQLModule` 相同的配置。
```typescript
import { Module } from '@nestjs/common';
import { GraphQLFederationModule } from '@nestjs/graphql';
import { UsersResolvers } from './users.resolvers';
import { UsersService } from './users.service'; // Not included in this example
@Module({
imports: [
GraphQLFederationModule.forRoot({
autoSchemaFile: true,
}),
],
providers: [UsersResolvers, UsersService],
})
export class AppModule {}
```
### 联合示例:Posts
我们的 Post 服务通过 `getPosts` 查询提供文章聚合,同时也使用 `user.posts` 来扩展我们的 `User` 类型。
### 模式优先
Posts 服务在它的 schema 中通过用 `extend` 关键字标记来引用 User 类型。它还向 User 类型添加了一个属性。请注意用于匹配 User 实例的 `@key` 指令,以及指示 `id` 字段在别处管理的 `@external` 指令。
```graphql
type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
user: User
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post]
}
extend type Query {
getPosts: [Post]
}
```
在我们的解析器这里有一个有趣的方法:`getUser()`。它返回一个引用,其中包含 `__typename` 和应用程序解析引用所需的任何其他属性,在这个例子中仅是一个属性 `id`。`__typename`被 GraphQL 网关用来精确定位负责 User 类型和请求实例的微服务。上面讨论的 Users 服务将在 `resolveReference()` 方法上被调用。
```typescript
import { Query, Resolver, Parent, ResolveField } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './posts.interfaces';
@Resolver('Post')
export class PostsResolvers {
constructor(private postsService: PostsService) {}
@Query('getPosts')
getPosts() {
return this.postsService.findAll();
}
@ResolveField('user')
getUser(@Parent() post: Post) {
return { __typename: 'User', id: post.userId };
}
}
```
Posts 服务几乎具有和 Users 相同的模块,但为了完整起见,我们在下面将它包含进来:
```typescript
import { Module } from '@nestjs/common';
import { GraphQLFederationModule } from '@nestjs/graphql';
import { PostsResolvers } from './posts.resolvers';
@Module({
imports: [
GraphQLFederationModule.forRoot({
typePaths: ['**/*.graphql'],
}),
],
providers: [PostsResolvers],
})
export class AppModule {}
```
### 代码优先
我们需要创建一个代表我们的 User 实体的类。即使它存在于其他服务中,我们也将使用和继承它。注意 `@extends` 和 `@external` 指令。
```typescript
import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';
@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
@Directive('@external')
id: number;
@Field((type) => [Post])
posts?: Post[];
}
```
我们在 `User` 实体上为我们的扩展创建解析器,如下所示:
```typescript
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';
@Resolver((of) => User)
export class UsersResolvers {
constructor(private readonly postsService: PostsService) {}
@ResolveField((of) => [Post])
public posts(@Parent() user: User): Post[] {
return this.postsService.forAuthor(user.id);
}
}
```
我们还需要创建我们的 `Post` 实体:
```typescript
import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { User } from './user.entity';
@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
@Field((type) => ID)
id: number;
@Field()
title: string;
@Field((type) => Int)
authorId: number;
@Field((type) => User)
user?: User;
}
```
还有它的解析器:
```typescript
import { Query, Args, ResolveField, Resolver, Parent } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';
@Resolver((of) => Post)
export class PostsResolvers {
constructor(private readonly postsService: PostsService) {}
@Query((returns) => Post)
findPost(@Args('id') id: number): Post {
return this.postsService.findOne(id);
}
@Query((returns) => [Post])
getPosts(): Post[] {
return this.postsService.all();
}
@ResolveField((of) => User)
user(@Parent() post: Post): any {
return { __typename: 'User', id: post.authorId };
}
}
```
最后,在模块中把它们串联起来。注意 schema 构建配置,在这里我们指定 `User` 为外部类型。
```typescript
import { Module } from '@nestjs/common';
import { GraphQLFederationModule } from '@nestjs/graphql';
import { User } from './user.entity';
import { PostsResolvers } from './posts.resolvers';
import { UsersResolvers } from './users.resolvers';
import { PostsService } from './posts.service'; // Not included in example
@Module({
imports: [
GraphQLFederationModule.forRoot({
autoSchemaFile: true,
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolvers, UsersResolvers, PostsService],
})
export class AppModule {}
```
### 联合示例:网关
首先,安装网关的依赖包:
```bash
$ npm install --save @apollo/gateway
```
我们的网关只需要一个端点列表,它会从那里自动发现所有的 schemas。因为代码和模式优先是一样的,所以网关的代码很短:
```typescript
import { Module } from '@nestjs/common';
import { GraphQLGatewayModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLGatewayModule.forRoot({
server: {
// ... Apollo server options
cors: true,
},
gateway: {
serviceList: [
{ name: 'users', url: 'http://user-service/graphql' },
{ name: 'posts', url: 'http://post-service/graphql' },
],
},
}),
],
})
export class AppModule {}
```
代码优先模式和架构优先模式在此处提供了[一个](https://github.com/nestjs/nest/tree/master/sample/32-graphql-federation-schema-first/gateway)[工作](https://github.com/nestjs/nest/tree/master/sample/31-graphql-federation-code-first/gateway)示例。[](https://github.com/nestjs/nest/tree/master/sample/32-graphql-federation-schema-first/gateway)
> Apollo 建议你不要依赖生产环境中的服务发现,而是使用它们的[图表管理器](https://www.apollographql.com/docs/federation/managed-federation/overview/)
### 与 ` Mercurius`联合
首先安装所需的依赖项:
~~~bash
$ npm install --save @apollo/subgraph @nestjs/mercurius
~~~
> **笔记**:`@apollo/subgraph` 包是构建子图模式(`buildSubgraphSchema`、`printSubgraphSchema` 函数)所必需的。
#### 架构优先
“用户服务”提供了一个简单的模式。请注意`@key` 指令:它指示 Mercurius 查询计划器,如果您指定其 id,则可以获取特定的 User 实例。另外,请注意我们扩展了 `Query` 类型。
~~~graphql
type User @key(fields: "id") {
id: ID!
name: String!
}
extend type Query {
getUser(id: ID!): User
}
~~~
`Resolver` 提供了另一种名为 `resolveReference()` 的方法。 每当相关资源需要用户实例时,此方法由 Mercurius 网关触发。 稍后我们将在 `Posts` 服务中看到一个这样的例子。 请注意,该方法必须使用 `@ResolveReference()` 装饰器进行注释。
~~~typescript
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { UsersService } from './users.service';
@Resolver('User')
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query()
getUser(@Args('id') id: string) {
return this.usersService.findById(id);
}
@ResolveReference()
resolveReference(reference: { __typename: string; id: string }) {
return this.usersService.findById(reference.id);
}
}
~~~
最后,我们通过在配置对象中注册传递 `MercuriusFederationDriver` 驱动程序的 `GraphQLModule` 来连接所有内容:
~~~typescript
import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { UsersResolver } from './users.resolver';
@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
typePaths: ['**/*.graphql'],
federationMetadata: true,
}),
],
providers: [UsersResolver],
})
export class AppModule {}
~~~
### 联合示例:Posts
我们的 Post 服务通过 `getPosts` 查询提供文章聚合,同时也使用 `user.posts` 来扩展我们的 `User` 类型。
#### 架构优先
`“Posts service”`通过用 extend 关键字标记它来引用其架构中的用户类型。 它还在用户类型(帖子)上声明了一个附加属性。 请注意用于匹配 `User `实例的 `@key` 指令,以及指示 id 字段在其他地方管理的 `@external `指令。
~~~graphql
type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
user: User
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post]
}
extend type Query {
getPosts: [Post]
}
~~~
在下面的示例中,PostsResolver 提供了 getUser() 方法,该方法返回包含 __typename 的引用和您的应用程序可能需要解析引用的一些附加属性,在本例中为 id。 __typename 被 GraphQL 网关用来查明负责用户类型的微服务并检索相应的实例。 执行 resolveReference() 方法时将请求上述“用户服务”。
~~~typescript
import { Query, Resolver, Parent, ResolveField } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './posts.interfaces';
@Resolver('Post')
export class PostsResolver {
constructor(private postsService: PostsService) {}
@Query('getPosts')
getPosts() {
return this.postsService.findAll();
}
@ResolveField('user')
getUser(@Parent() post: Post) {
return { __typename: 'User', id: post.userId };
}
}
~~~
最后,我们必须注册 `GraphQLModule`,类似于我们在“用户服务”部分中所做的。
~~~typescript
import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { PostsResolver } from './posts.resolver';
@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
federationMetadata: true,
typePaths: ['**/*.graphql'],
}),
],
providers: [PostsResolvers],
})
export class AppModule {}
~~~
#### 代码优先
首先,我们必须声明一个代表用户实体的类。 尽管实体本身存在于另一个服务中,但我们将在此处使用它(扩展其定义)。 注意@extends 和@external 指令。
~~~ts
import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';
@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
@Directive('@external')
id: number;
@Field((type) => [Post])
posts?: Post[];
}
~~~
现在让我们在 User 实体上为我们的扩展创建相应的解析器,如下所示:
~~~ts
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';
@Resolver((of) => User)
export class UsersResolver {
constructor(private readonly postsService: PostsService) {}
@ResolveField((of) => [Post])
public posts(@Parent() user: User): Post[] {
return this.postsService.forAuthor(user.id);
}
}
~~~
我们还必须定义 Post 实体类:
~~~ts
import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { User } from './user.entity';
@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
@Field((type) => ID)
id: number;
@Field()
title: string;
@Field((type) => Int)
authorId: number;
@Field((type) => User)
user?: User;
}
~~~
及其解析器:
~~~ts
import { Query, Args, ResolveField, Resolver, Parent } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';
@Resolver((of) => Post)
export class PostsResolver {
constructor(private readonly postsService: PostsService) {}
@Query((returns) => Post)
findPost(@Args('id') id: number): Post {
return this.postsService.findOne(id);
}
@Query((returns) => [Post])
getPosts(): Post[] {
return this.postsService.all();
}
@ResolveField((of) => User)
user(@Parent() post: Post): any {
return { __typename: 'User', id: post.authorId };
}
}
~~~
最后,将其捆绑在一个模块中。 请注意架构构建选项,我们在其中指定 User 是孤立(外部)类型。
~~~ts
import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { User } from './user.entity';
import { PostsResolvers } from './posts.resolvers';
import { UsersResolvers } from './users.resolvers';
import { PostsService } from './posts.service'; // Not included in example
@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
autoSchemaFile: true,
federationMetadata: true,
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}
~~~
### 联合示例:网关
网关需要指定一个端点列表,它将自动发现相应的模式。因此,对于代码优先和架构优先方法,网关服务的实现将保持不变。
~~~typescript
import {
MercuriusGatewayDriver,
MercuriusGatewayDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot<MercuriusGatewayDriverConfig>({
driver: MercuriusGatewayDriver,
gateway: {
services: [
{ name: 'users', url: 'http://user-service/graphql' },
{ name: 'posts', url: 'http://post-service/graphql' },
],
},
}),
],
})
export class AppModule {}
~~~
### 共享上下文
你可以通过一个构建服务来自定义网关和联合服务之间的请求。这让你能够共享有关请求的上下文。你能轻松继承默认的 `RemoteGraphQLDataSource` 并实现其中一个钩子。有关可能性的更多信息,请参阅 [Apollo 文档](https://www.apollographql.com/docs/federation/api/apollo-gateway/#class-remotegraphqldatasource)中的 `RemoteGraphQLDataSource` 章节.
```typescript
import { Module } from '@nestjs/common';
import { GATEWAY_BUILD_SERVICE, GraphQLGatewayModule } from '@nestjs/graphql';
import { RemoteGraphQLDataSource } from '@apollo/gateway';
import { decode } from 'jsonwebtoken';
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
async willSendRequest({ request, context }) {
const { userId } = await decode(context.jwt);
request.http.headers.set('x-user-id', userId);
}
}
@Module({
providers: [
{
provide: AuthenticatedDataSource,
useValue: AuthenticatedDataSource,
},
{
provide: GATEWAY_BUILD_SERVICE,
useFactory: (AuthenticatedDataSource) => {
return ({ name, url }) => new AuthenticatedDataSource({ url });
},
inject: [AuthenticatedDataSource],
},
],
exports: [GATEWAY_BUILD_SERVICE],
})
class BuildServiceModule {}
@Module({
imports: [
GraphQLGatewayModule.forRootAsync({
useFactory: async () => ({
gateway: {
serviceList: [
/* services */
],
},
server: {
context: ({ req }) => ({
jwt: req.headers.authorization,
}),
},
}),
imports: [BuildServiceModule],
inject: [GATEWAY_BUILD_SERVICE],
}),
],
})
export class AppModule {}
```
#### 代码优先
首先向 User 实体添加一些额外的装饰器。
~~~ts
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field((type) => ID)
id: number;
@Field()
name: string;
}
~~~
`Resolver` 提供了另一种名为 `resolveReference()` 的方法。 每当相关资源需要用户实例时,此方法由 `Mercurius` 网关触发。 稍后我们将在 `Posts` 服务中看到一个这样的例子。 请注意,该方法必须使用 `@ResolveReference()` 装饰器进行注释。
~~~ts
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query((returns) => User)
getUser(@Args('id') id: number): User {
return this.usersService.findById(id);
}
@ResolveReference()
resolveReference(reference: { __typename: string; id: number }): User {
return this.usersService.findById(reference.id);
}
}
~~~
最后,我们通过在配置对象中注册传递 MercuriusFederationDriver 驱动程序的 GraphQLModule 来连接所有内容:
~~~typescript
import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // Not included in this example
@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
autoSchemaFile: true,
federationMetadata: true,
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}
~~~
### 异步配置
联合服务和网关模块都支持使用同样的 `forRootAsync` 异步初始化,相关文档详见[快速开始](/8/graphql?id=async-配置)。
- 介绍
- 概述
- 第一步
- 控制器
- 提供者
- 模块
- 中间件
- 异常过滤器
- 管道
- 守卫
- 拦截器
- 自定义装饰器
- 基础知识
- 自定义提供者
- 异步提供者
- 动态模块
- 注入作用域
- 循环依赖
- 模块参考
- 懒加载模块
- 应用上下文
- 生命周期事件
- 跨平台
- 测试
- 技术
- 数据库
- Mongo
- 配置
- 验证
- 缓存
- 序列化
- 版本控制
- 定时任务
- 队列
- 日志
- Cookies
- 事件
- 压缩
- 文件上传
- 流式处理文件
- HTTP模块
- Session(会话)
- MVC
- 性能(Fastify)
- 服务器端事件发送
- 安全
- 认证(Authentication)
- 授权(Authorization)
- 加密和散列
- Helmet
- CORS(跨域请求)
- CSRF保护
- 限速
- GraphQL
- 快速开始
- 解析器(resolvers)
- 变更(Mutations)
- 订阅(Subscriptions)
- 标量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 联合类型
- 枚举(Enums)
- 字段中间件
- 映射类型
- 插件
- 复杂性
- 扩展
- CLI插件
- 生成SDL
- 其他功能
- 联合服务
- 迁移指南
- Websocket
- 网关
- 异常过滤器
- 管道
- 守卫
- 拦截器
- 适配器
- 微服务
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定义传输器
- 异常过滤器
- 管道
- 守卫
- 拦截器
- 独立应用
- Cli
- 概述
- 工作空间
- 库
- 用法
- 脚本
- Openapi
- 介绍
- 类型和参数
- 操作
- 安全
- 映射类型
- 装饰器
- CLI插件
- 其他特性
- 迁移指南
- 秘籍
- CRUD 生成器
- 热重载
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模块
- Swagger
- 健康检查
- CQRS
- 文档
- Prisma
- 静态服务
- Nest Commander
- 问答
- Serverless
- HTTP 适配器
- 全局路由前缀
- 混合应用
- HTTPS 和多服务器
- 请求生命周期
- 常见错误
- 实例
- 迁移指南
- 发现
- 谁在使用Nest?