联合(Federation)
联合提供了一种将您的单体GraphQL服务器拆分为独立的微服务的方式。 它包含两个组件:一个网关和一个或多个联合的微服务。 每个微服务都持有部分模式,而网关将这些模式合并成一个可以由客户端消耗的单一模式。
引用Apollo文档的话, 联合设计有以下核心原则:
- 构建图形应该是声明性的。使用联合,您可以从模式内部声明性地组合图形,而不是编写命令式的模式拼接代码。
- 代码应该按关注点分离,而不是按类型分离。通常没有单个团队控制像User或Product这样重要类型的每个方面,因此这些类型的定义应该分布在团队和代码库之间,而不是集中在一处。
- 图应该对客户端来说是简单的。联合服务可以共同形成一个完整、以产品为中心的图形,准确地反映了客户端上的使用方式。
- 它只是GraphQL,仅使用语言的规范特性。任何语言,不仅仅是JavaScript,都可以实现联合。
联合当前不支持订阅。
在以下部分中,我们将设置一个演示应用程序, 该应用程序包含一个网关和两个联合的端点:用户服务和帖子服务。
使用Apollo进行联合
首先安装所需的依赖:
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 Gateway需要User实例时,该方法将由它触发。
稍后我们将在帖子服务中看到这个方法的示例。
请注意,该方法必须用@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 Gateway需要User
实例时,该方法将由它触发。
稍后我们将在帖子服务中看到这个方法的示例。
请注意,该方法必须用@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
类型的user.posts
字段。
首先模式
"帖子服务"在其模式中通过使用extend
关键字标记User
类型来引用它。
它还在User
类型上声明了一个额外的属 性(posts
)。
请注意使用@key
指令匹配User
实例,并使用@external
指令指示id字段在其他地方进行管理。
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
是一个孤立的(external
)类型。
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
@apollo/subgraph
包是构建子图模式(buildSubgraphSchema
,
printSubgraphSchema
函数)所必需的。
模式优先方式
"用户服务"提供了一个简单的模式。
请注意@key
指令:它指示Mercurius查询规划器,
如果指定其id
,则可以获取User
的特定实例。还要注意,我们extend
了Query
类型。
type User @key(fields: "id") {
id: ID!
name: String!
}
extend type Query {
getUser(id: ID!): User
}
解析器提供了一个名为resolveReference()
的附加方法。
每当Mercurius Gateway需要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);
}
}
最后,我们通过在配置对象中传递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 Gateway需要User实例时,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);
}
}
最后,我们通过在配置对象中传递MercuriusFederationDriver
驱动程序来注册GraphQLModule
。
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类型的user.posts
字段。
模式优先模式
"帖子服务"在其模式中通过使用extend
关键字标记User类型来引用它。
它还在User
类型上声明了一个额外的属性(posts
)。
请注意使用@key
指令匹配User
实例,并使用@external
指令指示id
字段在其他地方进行管理。
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 {}
代码优先模式
首先,我们必须声明一个表示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
是一个孤立的(external
)类型。
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 {}
联合示例:网关
网关需要指定端点的列表,并且它将自动发现相应的模式。 因此,网关服务的实现对于代码和模式优先方法都将保持相同。
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
Federation 2改进了开发人员在原始Apollo联合(在本文档中称为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 {}