NestJS Logo

联邦

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
注意 构建子图模式(buildSubgraphSchemaprintSubgraphSchema函数)需要@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 {}

联邦示例:帖子#

出于与上述相同的原因,我们不再需要扩展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 {}

支持一下