Skip to main content

联合(Federation)

联合提供了一种将您的单体GraphQL服务器拆分为独立的微服务的方式。 它包含两个组件一个网关一个或多个联合的微服务。 每个微服务都持有部分模式,而网关将这些模式合并成一个可以由客户端消耗的单一模式。

引用Apollo文档的话, 联合设计有以下核心原则:

  1. 构建图形应该是声明性的。使用联合,您可以从模式内部声明性地组合图形,而不是编写命令式的模式拼接代码。
  2. 代码应该按关注点分离,而不是按类型分离。通常没有单个团队控制像User或Product这样重要类型的每个方面,因此这些类型的定义应该分布在团队和代码库之间,而不是集中在一处。
  3. 图应该对客户端来说是简单的。联合服务可以共同形成一个完整、以产品为中心的图形,准确地反映了客户端上的使用方式。
  4. 它只是GraphQL,仅使用语言的规范特性。任何语言,不仅仅是JavaScript,都可以实现联合。
warning

联合当前不支持订阅。

在以下部分中,我们将设置一个演示应用程序, 该应用程序包含一个网关和两个联合的端点:用户服务帖子服务

使用Apollo进行联合

首先安装所需的依赖:

npm install --save @apollo/federation @apollo/subgraph

模式优先

"用户服务"提供了一个简单的模式。 请注意@key指令:它指示Apollo查询规划器,如果指定了其id, 则可以获取User的特定实例。还请注意我们extendQuery类型。

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
tip

@apollo/subgraph包是构建子图模式(buildSubgraphSchemaprintSubgraphSchema函数)所必需的。

模式优先方式

"用户服务"提供了一个简单的模式。 请注意@key指令:它指示Mercurius查询规划器, 如果指定其id,则可以获取User的特定实例。还要注意,我们extendQuery类型。

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)中的体验, 它与大多数原始超图兼容。

warning

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 {}

联合示例:帖子

出于同样的原因,我们不再需要扩展UserQuery

模式优先方式

我们可以简单地从模式中删除extendexternal指令。

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中删除extendsexternal指令。

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 {}