【GraphQL】使用ApolloServer v4和Prisma实现Subscription时,获取相关数据的方法

首先

这次我们将通过GraphQL和Apollo Server v4实现在数据库中建立关系的数据的实时获取方法(Subscription)。

前提 (qian2 ti2)

如果不了解Subscription的人,请参考官方或以下的文章。

Apollo Server allows you to implement subscriptions using ApolloServer v4.

实施

我们将实现一项功能,即对帖子进行投票(类似于点赞功能)。

模式定义

作为注意事项,本次将以挑选相关部分而非整个代码的形式进行说明。

Prisma的模式
model Link {
  id          Int      @id @default(autoincrement())
  createdAt   DateTime @default(now())
  description String
  url         String
  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId      Int
  votes       Vote[]
}

model User {
  id       Int    @id @default(autoincrement())
  name     String
  email    String @unique
  password String
  links    Link[]
  votes    Vote[]
}

model Vote {
  id     Int  @id @default(autoincrement())
  link   Link @relation(fields: [linkId], references: [id], onDelete: Cascade)
  linkId Int
  user   User @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId Int

  @@unique([linkId, userId])
}

以上是数据库的模式(表)定义。投票由Vote模型进行管理。该模型具有User和Link模型作为其父模型,因此建立了相应的关系。

GraphQL模式(读作gurafuku-ru mo-xing)
type Mutation {
  vote(linkId: ID!): Vote
}

type Subscription {
  newVote: NewVote
}

type Link {
  id: ID!
  description: String!
  url: String!
  user: User!
  votes: [Vote!]!
}

type User {
  id: ID!
  name: String!
  email: String!
  links: [Link!]!
}

type Vote {
  id: ID!
  link: Link!
  user: User!
}

type NewVote {
  id: ID
  link: Link
  user: User
}

在GraphQL的架构中,我们同样对表格进行关联配置。

我想做的事情如下所述。

    • 投票をするとVoteテーブルにデータが格納される

 

    投票したらリアルタイムで投票時のデータを取得する

因此,我们在Mutation中指定了投票的Mutation,并在Subscription中指定了要获取的投票数据。

对于那些认为在”Subscription的newVote”中应该指定Vote类型的人,我在这里解释一下。 如果将Subscription的类型设置为Vote,那么只能获取到Vote的id,而如果尝试获取相关的User或Link,则会返回null并报错。(我已经实际测试过了)

因此,这有点麻烦,我在NewVote中进行了类型定义。
由于这种类型定义很容易被忽略,所以要注意。

定制变异
(Custom Mutation)

在这里,我们将实现用于检索建立了关系的数据的处理。
例如,检索与链接关联的用户等操作。

用户变异
import type { Context } from '@/types/Context'

export const links = (parent: { id: number }, __: unknown, context: Context) =>
  context.prisma.user
    .findUnique({
      where: { id: parent.id },
    })
    .links()

通过数组获取与用户关联的链接。
“parent”代表父级的意思,所以它指的是父级表的ID,也就是Links表的父级表ID。因此,我们要根据User表的ID进行搜索(虽然有些复杂,但就是这个意思)。

从数据库模式的角度来看,可以将其对应到“用户”的“链接”属性上。

type User {
  id: ID!
  name: String!
  email: String!
  links: [Link!]!
}
链接突变
import type { Context } from '@/types/Context'

export const user = (parent: { id: number }, __: unknown, context: Context) => {
  return context.prisma.link
    .findUnique({
      where: { id: parent.id },
    })
    .user()
}

export const votes = (parent: { id: number }, __: unknown, context: Context) =>
  context.prisma.link
    .findUnique({
      where: { id: parent.id },
    })
    .votes()

我们也通过Links提供了类似的功能,可以获取相关的数据。

根据模式定义来说,Link的用户和投票就是这个。

type Link {
  id: ID!
  description: String!
  url: String!
  user: User!
  votes: [Vote!]!
}
投票变异
import type { Context } from '@/types/Context'

export const link = (parent: { id: number }, __: unknown, context: Context) =>
  context.prisma.vote.findUnique({ where: { id: parent.id } }).link()

export const voteUser = (
  parent: { id: number },
  __: unknown,
  context: Context
) => context.prisma.vote.findUnique({ where: { id: parent.id } }).user()

与Vote类似,这是一个有1:N关联的子表,因此需要使用每个父表的id来获取数据。

根据架构定义,Vote的link和user对应于此。

type Vote {
  id: ID!
  link: Link!
  user: User!
}

实施投票功能

我們將開始實現以下基因突變和訂閱處理的詳細流程。

type Mutation {
  vote(linkId: ID!): Vote
}

type Subscription {
  newVote: NewVote
}
处理细节
export const vote = async (
  _: unknown,
  args: { linkId: string },
  context: Context
) => {
  const userId = context.userId as number

  const vote = await context.prisma.vote.findUnique({
    where: {
      linkId_userId: {
        linkId: Number(args.linkId),
        userId: userId,
      },
    },
  })

  if (vote) {
    throw new Error(`Link is already voted: ${args.linkId}`)
  }

  const newVote = await context.prisma.vote.create({
    data: {
      user: { connect: { id: userId } },
      link: { connect: { id: Number(args.linkId) } },
    },
    include: {
      link: true,
      user: true,
    },
  })

  context.pubsub.publish('NEW_VOTE', newVote)

  return newVote
}

我的工作内容如下。

    • contextよりuserIdを取得

 

    • すでに投票されているかどうかを判定

 

    • 投票する(Voteテーブルにデータを格納)

 

    サブスクリプションを送信(publish)する

在newVote中最重要的是include部分,将其设为true可以获取与注册相关的link和user。

另外,提到publish,它接收第一个参数作为触发器名称,第二个参数作为要传递的值。
也就是说,在订阅者端指定相同的触发器名称,就可以接收到newVote并实时获取它。

实施订阅模式

这是按照官方要求进行实现的。我将展示整个代码。

import 'dotenv/config'

import { ApolloServer } from '@apollo/server'
import { expressMiddleware } from '@apollo/server/express4'
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'
import { loadSchemaSync } from '@graphql-tools/load'
import { addResolversToSchema } from '@graphql-tools/schema'
import { PrismaClient } from '@prisma/client'
import bodyParser from 'body-parser'
import cors from 'cors'
import express from 'express'
import { PubSub } from 'graphql-subscriptions'
import { useServer } from 'graphql-ws/lib/use/ws'
import { createServer } from 'http'
import { join } from 'path'
import { WebSocketServer } from 'ws'

import { user, votes } from './resolvers/Link'
import { vote } from './resolvers/Mutation'
import { links } from './resolvers/User'
import { link, voteUser } from './resolvers/Vote'
import type { Context } from './types/Context'
import type { NewVote } from './types/NewVote'
import { getUserId } from './utils'

const PORT = 4000
const pubsub = new PubSub()

const prisma = new PrismaClient()

const app = express()

const schema = loadSchemaSync(join(__dirname, './schema.graphql'), {
  loaders: [new GraphQLFileLoader()],
})

// リゾルバー関数
const resolvers = {
  Mutation: {
    vote: vote,
  },

  Subscription: {
    newVote: {
      subscribe: () => pubsub.asyncIterator(['NEW_VOTE']),
      resolve: (payload: NewVote) => payload,
    },
  },

  Link: {
    user: user,
    votes: votes,
  },

  User: {
    links: links,
  },

  Vote: {
    link: link,
    user: voteUser,
  },
}

const schemaWithResolvers = addResolversToSchema({ schema, resolvers })

const httpServer = createServer(app)

const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
})

const serverCleanup = useServer({ schema: schemaWithResolvers }, wsServer)

const server = new ApolloServer<Context>({
  schema: schemaWithResolvers,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),

    {
      async serverWillStart() {
        return {
          async drainServer() {
            await serverCleanup.dispose()
          },
        }
      },
    },
  ],
})

;(async () => {
  try {
    await server.start()

    app.use(
      '/graphql',
      cors<cors.CorsRequest>(),
      bodyParser.json(),
      expressMiddleware(server, {
        context: async ({ req }) => ({
          ...req,
          prisma,
          pubsub,
          userId: req && req.headers.authorization ? getUserId(req) : undefined,
        }),
      })
    )

    httpServer.listen(PORT, () => {
      console.log(`? Query endpoint ready at http://localhost:${PORT}/graphql`)
      console.log(
        `? Subscription endpoint ready at ws://localhost:${PORT}/graphql`
      )
    })
  } catch (error) {
    console.error('Error starting server: ', error)
  }
})()
定义形状

以下是相关的类型定义。

import type { Prisma, PrismaClient } from '@prisma/client'
import type { DefaultArgs } from '@prisma/client/runtime'
import type { PubSub } from 'graphql-subscriptions'

export type Context = {
  prisma: PrismaClient<
    Prisma.PrismaClientOptions,
    never,
    Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined,
    DefaultArgs
  >
  pubsub: PubSub
  userId?: string | number
}
import type { Link, User } from '@prisma/client'

export type NewVote = {
  id: number
  link: Link
  user: User
}
关于订阅实施部分的说明

我将对以下部分进行解释。

const pubsub = new PubSub()

 Subscription: {
    newVote: {
      subscribe: () => pubsub.asyncIterator(['NEW_VOTE']),
      resolve: (payload: NewVote) => payload,
    },
  }

在这里,我们将进行接收端的定义。
通过向PubSub实例的asyncIterator指定发送方的触发器名称,我们将接收到订阅消息。

只订阅的定义就可以获取投票数据。也就是说,无法获取与用户或链接相关的数据,只能获取投票的id。

如果需要获取关联的数据,您需要将pyaload作为resolve返回。通过将其指定为NewVote类型,您就可以获取关联的数据。

以上,实现已经完成。

到此为止

当重新回顾这次的实现时,我意识到在GraphQL模式中,类型的定义非常重要。
我认为将其确保为机器能够准确识别的,可以说是以机器的立场去编程非常重要。

如果能对某人有所帮助,我将感到幸运。

请提供参考文献。

Apollo Server中的订阅功能
【GraphQL】使用Apollo Server v4实现订阅功能的方法

bannerAds