联邦
联邦
联邦提供了一种将单体 GraphQL 服务器拆分为独立微服务的方法。它由两个组件组成:一个网关和一个或多个联邦微服务。每个微服务持有模式的一部分,网关将这些模式合并为客户端可以使用的单一模式。
引用 Apollo 文档,联邦的设计基于以下核心原则:
- 构建图应该是声明式的。通过联邦,你可以在模式内声明式地组合图,而不是编写命令式的模式拼接代码。
- 代码应该按关注点分离,而不是按类型分离。通常没有单一团队控制像 User 或 Product 这样重要类型的每个方面,因此这些类型的定义应该分布在团队和代码库中,而不是集中化。
- 图对客户端来说应该简单易用。联邦服务可以共同形成一个完整的、以产品为中心的图,准确反映客户端的使用方式。
- 它就是 GraphQL,只使用语言的规范兼容特性。任何语言,不仅仅是 JavaScript,都可以实现联邦。
联邦目前不支持订阅。
在以下章节中,我们将设置一个演示应用程序,它由一个网关和两个联邦端点组成:用户服务和帖子服务。
使用 Apollo 的联邦
首先安装所需的依赖项:
$ npm install --save @apollo/subgraph
Schema first
"用户服务"提供了一个简单的模式。注意 @key 指令:它指示 Apollo 查询规划器,如果你指定了 id,就可以获取 User 的特定实例。另外,注意我们 extend 了 Query 类型。
type User @key(fields: "id") {
id: ID!
name: String!
}
extend type Query {
getUser(id: ID!): User
}
解析器提供了一个名为 resolveReference() 的额外方法。当相关资源需要 User 实例时,Apollo Gateway 会触发此方法。我们稍后会在帖子服务中看到这个例子。请注意,该方法必须用 @ResolveReference() 装饰器注解。
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);
}
}
最后,我们通过注册 GraphQLModule 并在配置对象中传递 ApolloFederationDriver 驱动程序来连接所有内容:
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { UsersResolver } from './users.resolver';
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
typePaths: ['**/*.graphql'],
}),
],
providers: [UsersResolver],
})
export class AppModule {}
Code first
首先为 User 实体添加一些额外的装饰器。
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field(() => ID)
id: number;
@Field()
name: string;
}
解析器提供了一个名为 resolveReference() 的额外方法。当相关资源需要 User 实例时,Apollo Gateway 会触发此方法。我们稍后会在帖子服务中看到这个例子。请注意,该方法必须用 @ResolveReference() 装饰器注解。
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Resolver(() => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query(() => User)
getUser(@Args('id') id: number): User {
return this.usersService.findById(id);
}
@ResolveReference()
resolveReference(reference: { __typename: string; id: number }) {
return this.usersService.findById(reference.id);
}
}
最后,我们通过注册 GraphQLModule 并在配置对象中传递 ApolloFederationDriver 驱动程序来连接所有内容:
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // 此示例中未包含
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: true,
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}
Code first 模式的工作示例可在这里找到,Schema first 模式的示例可在这里找到。
联邦示例:帖子
帖子服务应该通过 getPosts 查询提供聚合的帖子,同时还要用 user.posts 字段扩展我们的 User 类型。
Schema first
"帖子服务"通过用 extend 关键字标记来引用其模式中的 User 类型。它还在 User 类型上声明了一个额外的属性(posts)。注意用于匹配 User 实例的 @key 指令,以及表示 id 字段在其他地方管理的 @external 指令。
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 Gateway 用来确定负责 User 类型的微服务并检索相应的实例。上面描述的"用户服务"将在执行 resolveReference() 方法时被请求。
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,类似于我们在"用户服务"部分所做的。
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { PostsResolver } from './posts.resolver';
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
typePaths: ['**/*.graphql'],
}),
],
providers: [PostsResolvers],
})
export class AppModule {}
Code first
首先,我们必须声明一个表示 User 实体的类。虽然实体本身存在于另一个服务中,但我们将在这里使用它(扩展其定义)。注意 @extends 和 @external 指令。
import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';
@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
@Field(() => ID)
@Directive('@external')
id: number;
@Field(() => [Post])
posts?: Post[];
}
现在让我们为 User 实体上的扩展创建相应的解析器,如下所示:
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly postsService: PostsService) {}
@ResolveField(() => [Post])
public posts(@Parent() user: User): Post[] {
return this.postsService.forAuthor(user.id);
}
}
我们还必须定义 Post 实体类:
import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { User } from './user.entity';
@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
@Field(() => ID)
id: number;
@Field()
title: string;
@Field(() => Int)
authorId: number;
@Field(() => User)
user?: User;
}
以及它的解析器:
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(() => Post)
export class PostsResolver {
constructor(private readonly postsService: PostsService) {}
@Query(() => Post)
findPost(@Args('id') id: number): Post {
return this.postsService.findOne(id);
}
@Query(() => [Post])
getPosts(): Post[] {
return this.postsService.all();
}
@ResolveField(() => User)
user(@Parent() post: Post): any {
return { __typename: 'User', id: post.authorId };
}
}
最后,在模块中将它们绑定在一起。注意模式构建选项,我们在其中指定 User 是一个孤立的(外部)类型。
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
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'; // 示例中未包含
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: true,
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}
Code first 模式的工作示例可在这里找到,Schema first 模式的示例可在这里找到。
联邦示例:网关
首先安装所需的依赖项:
$ npm install --save @apollo/gateway
网关需要指定端点列表,它将自动发现相应的模式。因此,网关服务的实现对于 code first 和 schema first 方法都保持相同。
import { IntrospectAndCompose } from '@apollo/gateway';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot<ApolloGatewayDriverConfig>({
driver: ApolloGatewayDriver,
server: {
// ... Apollo 服务器选项
cors: true,
},
gateway: {
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://user-service/graphql' },
{ name: 'posts', url: 'http://post-service/graphql' },
],
}),
},
}),
],
})
export class AppModule {}
Code first 模式的工作示例可在这里找到,Schema first 模式的示例可在这里找到。
使用 Mercurius 的联邦
首先安装所需的依赖项:
$ npm install --save @apollo/subgraph @nestjs/mercurius
需要 @apollo/subgraph 包来构建子图模式(buildSubgraphSchema、printSubgraphSchema 函数)。
Schema first
"用户服务"提供了一个简单的模式。注意 @key 指令:它指示 Mercurius 查询规划器,如果你指定了 id,就可以获取 User 的特定实例。另外,注意我们 extend 了 Query 类型。
type User @key(fields: "id") {
id: ID!
name: String!
}
extend type Query {
getUser(id: ID!): User
}
解析器提供了一个名为 resolveReference() 的额外方法。当相关资源需要 User 实例时,Mercurius Gateway 会触发此方法。我们稍后会在帖子服务中看到这个例子。请注意,该方法必须用 @ResolveReference() 装饰器注解。
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);
}
}
最后,我们通过注册 GraphQLModule 并在配置对象中传递 MercuriusFederationDriver 驱动程序来连接所有内容:
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 {}
Code first
首先为 User 实体添加一些额外的装饰器。
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field(() => ID)
id: number;
@Field()
name: string;
}
解析器提供了一个名为 resolveReference() 的额外方法。当相关资源需要 User 实例时,Mercurius Gateway 会触发此方法。我们稍后会在帖子服务中看到这个例子。请注意,该方法必须用 @ResolveReference() 装饰器注解。
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';
@Resolver(() => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query(() => 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);
}
}
最后,我们通过注册 GraphQLModule 并在配置对象中传递 MercuriusFederationDriver 驱动程序来连接所有内容:
import {
MercuriusFederationDriver,
MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // 此示例中未包含
@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
autoSchemaFile: true,
federationMetadata: true,
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}
联邦示例:帖子
帖子服务应该通过 getPosts 查询提供聚合的帖子,同时还要用 user.posts 字段扩展我们的 User 类型。
Schema first
"帖子服务"通过用 extend 关键字标记来引用其模式中的 User 类型。它还在 User 类型上声明了一个额外的属性(posts)。注意用于匹配 User 实例的 @key 指令,以及表示 id 字段在其他地方管理的 @external 指令。
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 Gateway 用来确定负责 User 类型的微服务并检索相应的实例。上面描述的"用户服务"将在执行 resolveReference() 方法时被请求。
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,类似于我们在"用户服务"部分所做的。
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 {}
Code first
首先,我们必须声明一个表示 User 实体的类。虽然实体本身存在于另一个服务中,但我们将在这里使用它(扩展其定义)。注意 @extends 和 @external 指令。
import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';
@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
@Field(() => ID)
@Directive('@external')
id: number;
@Field(() => [Post])
posts?: Post[];
}
现在让我们为 User 实体上的扩展创建相应的解析器,如下所示:
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
import { User } from './user.entity';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly postsService: PostsService) {}
@ResolveField(() => [Post])
public posts(@Parent() user: User): Post[] {
return this.postsService.forAuthor(user.id);
}
}
我们还必须定义 Post 实体类:
import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { User } from './user.entity';
@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
@Field(() => ID)
id: number;
@Field()
title: string;
@Field(() => Int)
authorId: number;
@Field(() => User)
user?: User;
}
以及它的解析器:
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(() => Post)
export class PostsResolver {
constructor(private readonly postsService: PostsService) {}
@Query(() => Post)
findPost(@Args('id') id: number): Post {
return this.postsService.findOne(id);
}
@Query(() => [Post])
getPosts(): Post[] {
return this.postsService.all();
}
@ResolveField(() => User)
user(@Parent() post: Post): any {
return { __typename: 'User', id: post.authorId };
}
}
最后,在模块中将它们绑定在一起。注意模式构建选项,我们在其中指定 User 是一个孤立的(外部)类型。
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'; // 示例中未包含
@Module({
imports: [
GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
driver: MercuriusFederationDriver,
autoSchemaFile: true,
federationMetadata: true,
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}
联邦示例:网关
网关需要指定端点列表,它将自动发现相应的模式。因此,网关服务的实现对于 code first 和 schema first 方法都保持相同。
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 {}
Federation 2
引用 Apollo 文档,Federation 2 改进了原始 Apollo Federation(在本文档中称为 Federation 1)的开发者体验,它与大多数原始超图向后兼容。
Mercurius 不完全支持 Federation 2。你可以在这里查看支持 Federation 2 的库列表。
在以下章节中,我们将把之前的示例升级到 Federation 2。
联邦示例:用户
Federation 2 的一个变化是实体没有原始子图,所以我们不再需要扩展 Query。更多详细信息请参考 Apollo Federation 2 文档中的实体主题。
Schema first
我们可以简单地从模式中删除 extend 关键字。
type User @key(fields: "id") {
id: ID!
name: String!
}
type Query {
getUser(id: ID!): User
}
Code first
要使用 Federation 2,我们需要在 autoSchemaFile 选项中指定联邦版本。
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // 此示例中未包含
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: {
federation: 2,
},
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}
联邦示例:帖子
出于与上述相同的原因,我们不再需要扩展 User 和 Query。
Schema first
我们可以简单地从模式中删除 extend 和 external 指令
type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
user: User
}
type User @key(fields: "id") {
id: ID!
posts: [Post]
}
type Query {
getPosts: [Post]
}
Code first
由于我们不再扩展 User 实体,我们可以简单地从 User 中删除 extends 和 external 指令。
import { Directive, ObjectType, Field, ID } from '@nestjs/graphql';
import { Post } from './post.entity';
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field(() => ID)
id: number;
@Field(() => [Post])
posts?: Post[];
}
同样,类似于用户服务,我们需要在 GraphQLModule 中指定使用 Federation 2。
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
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'; // 示例中未包含
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: {
federation: 2,
},
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}