联合 Federation

导读

联合提供了一种将单体 GraphQL 服务器拆分为独立微服务的方法。它由两个组件组成:网关和一个或多个联合微服务。每个微服务都包含部分架构,网关将架构合并为客户端可以使用的单个架构。

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

  • 构建图表应具有声明性。使用联合,您可以从架构内部以声明方式编写图表,而不是编写命令式架构拼接代码。
  • 代码应按关注点而不是按类型分开。通常没有一个团队可以控制重要类型(如用户或产品)的每个方面,因此这些类型的定义应分布在团队和代码库中,而不是集中进行。
  • 图表应简单易用,方便客户端使用。总之,联合服务可以形成一个完整的、以产品为中心的图表,准确反映它在客户端的使用情况。
  • 它只是 GraphQL,仅使用符合规范的语言功能。任何语言(不仅仅是 JavaScript)都可以实现联合。
警告

联合目前不支持订阅。

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

与 Apollo 联合

首先安装所需的依赖项:

bash
$ npm install --save @apollo/subgraph

架构优先

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

graphql
type User @key(fields: "id") {
  id: ID!
  name: String!
}

extend type Query {
  getUser(id: ID!): User
}

Resolver 提供了一个名为 resolveReference() 的附加方法。每当相关资源需要 User 实例时,Apollo Gateway 就会触发此方法。稍后我们将在 Posts 服务中看到一个示例。请注意,该方法必须使用 @ResolveReference() 装饰器进行注释。

ts
import { Args, Query, ResolveReference, Resolver } 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驱动程序来连接所有内容:

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

先写代码

首先为用户实体添加一些额外的装饰器。

ts
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql'

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field(type => ID)
  id: number

  @Field()
  name: string
}

Resolver 提供了一个名为 resolveReference() 的附加方法。每当相关资源需要 User 实例时,Apollo Gateway 就会触发此方法。稍后我们将在 Posts 服务中看到一个示例。请注意,该方法必须使用 @ResolveReference() 装饰器进行注释。

ts
import { Args, Query, ResolveReference, Resolver } 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)
  }
}

最后,我们通过在配置对象中注册GraphQLModule并传递ApolloFederationDriver驱动程序来连接所有内容:

ts
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 指令。

graphql
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。GraphQL Gateway 使用 __typename 来精确定位负责 User 类型的微服务并检索相应的实例。执行 resolveReference() 方法时将请求上述用户服务

ts
import { Parent, Query, ResolveField, Resolver } 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,类似于我们在用户服务部分所做的。

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

代码优先

首先,我们必须声明一个代表用户实体的类。尽管实体本身存在于另一个服务中,但我们将在这里使用它(扩展其定义)。请注意@extends@external指令。

ts
import { Directive, Field, ID, ObjectType } 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实体上的扩展创建相应的解析器,如下所示:

ts
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实体类:

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

及其解析器:

ts
import { Args, Parent, Query, ResolveField, Resolver } 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是孤立(外部)类型。

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

对于代码优先模式,此处 提供了一个工作示例;对于架构优先模式,此处 提供了一个工作示例。

联合示例:网关

首先安装所需的依赖项:

bash
$ npm install --save @apollo/gateway

网关需要指定端点列表,它将自动发现相应的架构。因此,对于代码优先和架构优先方法,网关服务的实现将保持不变。

ts
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 联合

首先安装所需的依赖项:

bash
$ npm install --save @apollo/subgraph @nestjs/mercurius
注意

需要 @apollo/subgraph 包来构建子图模式(buildSubgraphSchemaprintSubgraphSchema 函数)。

模式优先

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

graphql
type User @key(fields: "id") {
  id: ID!
  name: String!
}

extend type Query {
  getUser(id: ID!): User
}

Resolver 提供了一个名为resolveReference()的附加方法。每当相关资源需要 User 实例时,Mercurius Gateway 就会触发此方法。稍后我们将在 Posts 服务中看到此示例。请注意,该方法必须使用@ResolveReference()装饰器进行注释。

ts
import { Args, Query, ResolveReference, Resolver } 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 驱动程序来连接一切:

ts
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 实体添加一些额外的装饰器。

ts
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql'

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field(type => ID)
  id: number

  @Field()
  name: string
}

Resolver 提供了一个名为 resolveReference() 的附加方法。每当相关资源需要 User 实例时,Mercurius Gateway 就会触发此方法。稍后我们将在 Posts 服务中看到此示例。请注意,该方法必须使用 @ResolveReference() 装饰器进行注释。

ts
import { Args, Query, ResolveReference, Resolver } 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)
  }
}

最后,我们通过在配置对象中注册 GraphQLModule 并传递 MercuriusFederationDriver 驱动程序来连接一切:

ts
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 指令。

graphql
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。GraphQL Gateway 使用 __typename 来精确定位负责 User 类型的微服务并检索相应的实例。执行 resolveReference() 方法时将请求上述用户服务

ts
import { Parent, Query, ResolveField, Resolver } 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,类似于我们在用户服务部分中所做的操作。

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

代码优先

首先,我们必须声明一个代表用户实体的类。尽管实体本身存在于另一个服务中,但我们将在这里使用它(扩展其定义)。请注意@extends@external指令。

ts
import { Directive, Field, ID, ObjectType } 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实体上的扩展创建相应的解析器,如下所示:

ts
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实体类:

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

及其解析器:

ts
import { Args, Parent, Query, ResolveField, Resolver } 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是孤立(外部)类型。

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

联合示例:网关

网关需要指定端点列表,它将自动发现相应的架构。因此,对于代码和架构优先方法,网关服务的实现将保持不变。

ts
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 联邦(本文档中称为联邦 1)的开发人员体验,与大多数原始超级图向后兼容。

警告

Mercurius 不完全支持联邦 2。您可以在 此处 查看支持联邦 2 的库列表。

在以下部分中,我们将把前面的示例升级到联邦 2。

联邦示例:用户

联邦 2 中的一个变化是实体没有原始子图,因此我们不再需要扩展查询。有关更多详细信息,请参阅 Apollo Federation 2 文档中的 实体主题

架构优先

我们可以简单地从架构中删除extend关键字。

graphql
type User @key(fields: "id") {
  id: ID!
  name: String!
}

type Query {
  getUser(id: ID!): User
}

代码优先

要使用 Federation 2,我们需要在autoSchemaFile选项中指定联合版本。

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

联合示例:帖子

出于与上述相同的原因,我们不再需要扩展用户查询

架构优先

我们可以简单地从架构中删除扩展外部指令

graphql
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指令。

ts
import { Directive, Field, ID, ObjectType } 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。

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