联邦
Federation提供了一种将您的单体GraphQL服务器拆分为独立的微服务的方式。它由两个组件组成:一个网关和一个或多个联合的微服务。每个微服务都保存了模式的一部分,而网关将这些模式合并成一个单一的模式,客户端可以消费这个模式。
引用Apollo文档中的描述,Federation遵循以下核心原则:
- 构建图形应该是声明性的。使用联合,您可以从模式内部声明性地组合图形,而无需编写命令式的模式拼接代码。
- 代码应该按关注点分隔,而不是按类型分隔。通常,没有一个团队可以控制重要类型(如用户或产品)的每个方面,因此这些类型的定义应该分布在团队和代码库之间,而不是集中在一起。
- 对客户端来说,图形应该是简单易懂的。联合的服务可以一起形成一个完整的、以产品为中心的图形,准确地反映了在客户端上的使用方式。
- 它只是GraphQL,仅使用语言规范兼容的语言特性。任何语言,不仅限于JavaScript,都可以实现联合。
警告 目前联合不支持订阅。
在接下来的章节中,我们将建立一个演示应用程序,该应用程序由一个网关和两个联合的端点组成:用户服务和帖子服务。
与Apollo的联邦#
Start by installing the required dependencies:
$ npm install --save @apollo/federation @apollo/subgraph
模式优先#
"用户服务"提供了一个简单的模式。请注意@key
指令:它指示Apollo查询规划器,如果指定了其id
,可以获取User
的特定实例。此外,请注意我们是如何extend
(扩展)Query
类型的。
type User @key(fields: "id") {
id: ID!
name: String!
}
extend type Query {
getUser(id: ID!): User
}
解析器提供了一个名为resolveReference()
的额外方法。每当Apollo网关需要一个相关资源的用户实例时,该方法将被触发。稍后我们将在帖子服务中看到这个方法的示例。请注意,这个方法必须用@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);
}
}
最后,我们通过在配置对象中传递ApolloFederationDriver
驱动程序来注册GraphQLModule
,将所有内容连接起来:
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 {}
代码优先#
首先,在User
实体上添加一些额外的装饰器。
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()
的附加方法。每当Apollo网关需要一个相关资源的用户实例时,该方法就会被Apollo Gateway触发调用。稍后我们将在帖子服务中看到这个方法的示例。请注意,这个方法必须使用@ResolveReference()
装饰器进行注解。
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);
}
}
最后,我们通过在配置对象中传递ApolloFederationDriver
驱动程序来注册GraphQLModule
,将所有内容连接起来:
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // Not included in this example
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: true,
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}
可以在这里找到一个可运行的示例:
联邦示例:帖子#
"帖子服务"应该通过getPosts
查询来提供聚合的帖子,还应该通过user.posts
字段来扩展我们的User
类型。
模式优先#
"帖子服务"在其模式中通过使用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网关用于确定负责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 {}
代码优先#
首先,我们需要声明一个代表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((type) => ID)
@Directive('@external')
id: number;
@Field((type) => [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((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
实体类:
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;
}
以及它的解析器:
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
是一个孤立(外部)类型。
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'; // Not included in example
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: true,
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}
一个可工作的示例可以在以下链接中找到:
联邦示例:网关#
首先安装所需的依赖:
$ npm install --save @apollo/gateway
网关需要指定端点列表,并且它将自动发现相应的模式。因此,无论是代码优先还是模式优先的方法,网关服务的实现都将保持不变。
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 server options
cors: true,
},
gateway: {
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://user-service/graphql' },
{ name: 'posts', url: 'http://post-service/graphql' },
],
}),
},
}),
],
})
export class AppModule {}
一个可工作的示例可以在以下链接中找到:
与Mercurius的联邦#
开始之前,请先安装所需的依赖:
$ npm install --save @apollo/subgraph @nestjs/mercurius
注意 构建子图模式(buildSubgraphSchema
,printSubgraphSchema
函数)需要@apollo/subgraph
包。
模式优先#
"用户服务"提供了一个简单的模式。请注意@key
指令:它指示Mercurius查询规划器,如果指定了其id
,可以获取User
的特定实例。此外,请注意我们是如何extend
(扩展)Query
类型的。
type User @key(fields: "id") {
id: ID!
name: String!
}
extend type Query {
getUser(id: ID!): User
}
解析器提供了一个名为resolveReference()
的额外方法。每当Mercurius网关需要一个相关资源的用户实例时,该方法将被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);
}
}
最后,我们通过在配置对象中传递MercuriusFederationDriver
驱动程序来注册GraphQLModule
,将所有内容连接起来:
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 {}
代码优先#
首先,在User
实体上添加一些额外的装饰器。
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()
的额外方法。每当Mercurius网关需要一个相关资源的用户实例时,该方法将被Mercurius Gateway触发。稍后我们将在帖子服务中看到这个方法的示例。请注意,这个方法必须使用@ResolveReference()
装饰器进行注解。
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
,将所有内容连接起来:
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 {}
联邦示例:帖子#
"帖子服务"应该通过getPosts
查询来提供聚合的帖子,还应该通过user.posts
字段来扩展我们的User
类型。
模式优先#
"帖子服务"通过使用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网关用于定位负责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 {}
代码优先#
首先,我们需要声明一个代表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((type) => ID)
@Directive('@external')
id: number;
@Field((type) => [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((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
实体类:
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;
}
以及它的解析器:
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
是一个孤立(外部)类型。
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 {}
联邦示例:网关#
网关需要指定端点列表,并且它将自动发现相应的模式。因此,无论是代码优先还是模式优先的方法,网关服务的实现都将保持不变。
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 {}
联邦2
引用Apollo docs中的描述,Federation 2改进了开发人员在原始Apollo Federation(在本文档中称为Federation 1)中的体验,它与大多数原始超图向后兼容。
警告 Mercurius不完全支持Federation 2。您可以在这里看到支持Federation 2的库列表。
在接下来的章节中,我们将升级之前的示例到Federation 2。
联邦示例:用户#
Federation 2中的一个变化是实体不再具有原始子图,因此我们不再需要扩展Query
。有关更多详细信息,请参阅Apollo Federation 2文档中的实体主题。
模式优先#
我们可以直接从模式中删除extend
关键字。
type User @key(fields: "id") {
id: ID!
name: String!
}
type Query {
getUser(id: ID!): User
}
代码优先#
要使用Federation 2,我们需要在autoSchemaFile
选项中指定联合版本。
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service'; // Not included in this example
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: {
federation: 2,
},
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}
联邦示例:帖子#
出于与上述相同的原因,我们不再需要扩展User
和Query
。
模式优先#
我们可以直接从模式中删除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]
}
代码优先#
由于我们不再扩展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((type) => ID)
id: number;
@Field((type) => [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'; // Not included in example
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: {
federation: 2,
},
buildSchemaOptions: {
orphanedTypes: [User],
},
}),
],
providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}