Prisma 1 GraphQL 订阅(Subscriptions)完全指南:实时监听数据变更的 API 实战与底层原理
【免费下载链接】prisma1💾 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL & MongoDB) [deprecated]项目地址: https://gitcode.com/gh_mirrors/pr/prisma1
GraphQL 订阅(Subscriptions)是 Prisma 1 数据 API(即生成的 Prisma GraphQL API)中用于实时接收数据变更通知的核心能力。本指南以 docs/1.11/04-Reference/03-Prisma-API/05-Subscriptions.md 为骨架,结合仓库中server/servers/subscriptions模块的 Scala 源码与测试用例,系统讲解订阅触发事件、类型订阅(created/updated/deleted)、WebSocket 协议交互、节点与字段过滤、关系订阅的 workaround 以及多订阅组合等完整实战方案。读完你将能够:通过 GraphQL Playground 或原生 WebSocket 建立订阅连接,按节点、字段与变更类型精确过滤订阅事件,并理解订阅事件从数据库变更到推送给客户端之间的完整链路。
概述:什么触发了订阅
GraphQL 订阅让你在数据发生变化时实时收到通知。Prisma 的数据 API 定义了三种触发订阅的事件(events):
- 一个新节点被创建(
CREATED) - 一个已有节点被更新(
UPDATED) - 一个已有节点被删除(
DELETED)
下面是一个典型的订阅示例:每当有新的Post节点被创建时,服务器推送的 payload 会包含该Post的description和imageUrl字段:
subscription newPosts { post(where: { mutation_in: [CREATED] }) { mutation node { description imageUrl } } }订阅使用一个专用的 WebSocket 端点(websocket endpoint)进行管理,而不是普通的 HTTP 查询端点。
可用的订阅清单如下(可通过服务内的 GraphQL Playground 探索):
- 对于数据模型中的每一个对象类型(object type),都会自动生成一个类型订阅(type subscription),用于监听该类型上的数据变更;
- 目前,在关系(relation)中连接或断开节点不会触发订阅!后文会介绍一种基于
UPDATED订阅的 workaround(见“关系订阅”一节)。
你可以在一个订阅请求中组合多个订阅触发器,精确控制希望收到通知的事件;订阅 API 同样复用了查询(Queries)中提供的强大过滤系统。
订阅请求:如何发送订阅
订阅请求可以通过以下方式发送:
- Apollo Client:使用
apollo-link-ws库可以方便地发起订阅(这是 GraphQL 生态中最常用的做法,社区中有大量基于 React 的实时聊天类示例项目可以参照); - GraphQL Playground:Playground 内建对订阅的支持,可直接探索和运行订阅;
- 任意 WebSocket 客户端:按下面的“原生 WebSockets”一节手动实现。
使用 Playground
GraphQL Playground 可以用来探索和运行 GraphQL 订阅。它会在内部自动完成 WebSocket 握手、subscription_start发送与subscription_data接收的流程,是调试订阅最快捷的方式。
使用原生 WebSockets(五步流程)
订阅通过 WebSocket 管理。完整流程分为以下五步,每一步都对应着订阅协议(graphql-subscriptions)中一个特定的消息类型。仓库源码server/servers/subscriptions/src/main/scala/com/prisma/subscriptions/protocol/SubscriptionProtocol.scala中SubscriptionProtocolV05定义了这套协议的完整消息集,其协议名即为graphql-subscriptions。
1. 建立连接
首先建立 WebSocket 连接,并指定graphql-subscriptions协议:
let webSocket = new WebSocket('wss://__CLUSTER__.prisma.sh/__WORKSPACE__/__SERVICE__/__STAGE__', 'graphql-subscriptions');其中 URL 中的__CLUSTER__、__WORKSPACE__、__SERVICE__、__STAGE__需要替换为你的实际集群地址、工作空间、服务名与阶段名。
2. 发起握手
监听open事件,然后向服务器发送一条type为init的 JSON 消息完成握手:
webSocket.onopen = (event) => { const message = { type: 'init' } webSocket.send(JSON.stringify(message)) }在源码协议定义中,这对应客户端 → 服务器的INIT消息(val INIT = "init" // Client -> Server)。
3. 接收消息
服务器会以不同的type属性返回多种消息,你可以针对每种消息做出相应处理:
webSocket.onmessage = (event) => { const data = JSON.parse(event.data) switch (data.type) { case 'init_success': { console.log('init_success, the handshake is complete') break } case 'init_fail': { throw { message: 'init_fail returned from WebSocket server', data } } case 'subscription_data': { console.log('subscription data has been received', data) break } case 'subscription_success': { console.log('subscription_success') break } case 'subscription_fail': { throw { message: 'subscription_fail returned from WebSocket server', data } } } }这些消息类型与源码中SubscriptionProtocolV05.MessageTypes的服务器 → 客户端消息一一对应:init_success(握手成功)、init_fail(握手失败)、subscription_success(订阅建立成功)、subscription_fail(订阅建立失败)、subscription_data(订阅数据推送)。此外协议还定义了keepalive心跳消息,用于保持连接活性。
4. 订阅数据变更
发送type为subscription_start的消息来订阅数据变更:
const message = { id: '1', type: 'subscription_start', query: ` subscription newPosts { post(filter: { mutation_in: [CREATED] }) { mutation node { description imageUrl } } } ` } webSocket.send(JSON.stringify(message))之后你会收到一条subscription_success消息;当数据发生变化时,则会收到subscription_data消息。subscription_start消息中携带的id属性会出现在所有subscription_data消息中,因此你可以在一条 WebSocket 连接上多路复用(multiplex)多个订阅,通过id区分不同订阅的数据。源码中SubscriptionsManagerForModel.Requests.StartSubscription以id: StringOrInt唯一标识一个订阅,正是对这种多路复用能力的支撑。
5. 取消订阅
发送type为subscription_end的消息即可取消订阅:
const message = { id: '1', type: 'subscription_end' } webSocket.send(JSON.stringify(message))在源码中,这对应SUBSCRIPTION_END消息,SubscriptionsManagerForModel收到EndSubscription请求后会把对应id的订阅从订阅集合中移除。
补充:仓库同时实现了
graphql-ws协议(SubscriptionProtocolV07,即 Apollosubscriptions-transport-ws现代版本所用的connection_init/start/stop/data/complete消息体系)。使用新版 Apollo 生态时,订阅请求与响应将遵循这套消息语义,其连接初始化、开始订阅、停止订阅的流程与本节的五步流程在概念上一一对应。
类型订阅:监听某个对象类型上的数据变更
对于数据模型中每一个对象类型,Prisma 会自动生成对应的类型订阅。以如下仅包含Post类型的数据模型为例:
type Post { id: ID! @unique title: String! description: String }在生成的 Prisma API 中,将出现一个post订阅,用于在Post类型的节点被创建、更新或删除时通知你。
订阅新创建的节点
订阅所有被创建的节点
使用where对象并设置mutation_in: [CREATED]:
subscription { post(where: { mutation_in: [CREATED] }) { mutation node { description imageUrl author { id } } } }payload 包含:
mutation:此处返回CREATED;node:允许你查询被创建节点的信息(以及其关联节点的信息)。
订阅特定的被创建节点
利用与查询相同的过滤系统(filter system),通过where对象中的node参数进一步过滤。例如,仅当某位特定用户关注(follows)了author时才通知你Post被创建:
subscription { post(where: { AND: [{ mutation_in: [CREATED] }, { node: { author: { followedBy_some: { id: "cj03x3nacox6m0119755kmcm3" } } }] }) { mutation node { description imageUrl author { id } } } }订阅被删除的节点
订阅所有被删除的节点
设置mutation_in: [DELETED]:
subscription deletePost { post(where: { mutation_in: [DELETED] }) { mutation previousValues { id } } }payload 包含:
mutation:此处返回DELETED;previousValues:节点删除前的标量值(scalar values)。
注意:对于
CREATED订阅,previousValues始终为null。
订阅特定的被删除节点
同样可以使用node参数做精确过滤。例如,仅当特定用户关注了author时才通知Post被删除:
subscription { post(where: { mutation_in: [DELETED] node: { author: { followedBy_some: { id: "cj03x3nacox6m0119755kmcm3" } } } }) { mutation previousValues { id } } }订阅被更新的节点
订阅所有被更新的节点
设置mutation_in: [UPDATED]:
subscription { post(where: { mutation_in: [UPDATED] }) { mutation node { description imageUrl author { id } } updatedFields previousValues { description imageUrl } } }payload 包含:
mutation:此处返回UPDATED;node:允许你查询被更新节点及其关联节点的信息;updatedFields:发生变更的字段列表;previousValues:节点更新前的标量值。
注意:对于
CREATED和DELETED订阅,updatedFields始终为null;对于CREATED订阅,previousValues始终为null。
订阅特定字段的更新
通过updatedFields_contains等过滤条件监听特定字段的更新。例如,仅当Post的description字段被修改时才通知你:
subscription { post(where: { mutation_in: [UPDATED] updatedFields_contains: "description" }) { mutation node { description } updatedFields previousValues { description } } }与updatedFields_contains类似的过滤条件还有:
updatedFields_contains_every: [String!]:当所有指定的字段都被更新时匹配;updatedFields_contains_some: [String!]:当部分指定字段被更新时匹配。
注意:
updatedFields系列过滤条件不能与mutation_in: [CREATED]或mutation_in: [DELETED]同时使用!
从源码实现看,previousValues与updatedFields在事件处理时有着明确的区分:server/servers/subscriptions/src/main/scala/com/prisma/subscriptions/resolving/SubscriptionResolver.scala中,handleDatabaseCreateEvent以previousValues = None, updatedFields = None执行查询;handleDatabaseUpdateEvent会带上previousValues与changedFields;handleDatabaseDeleteEvent则只带previousValues(updatedFields = None)。这从底层印证了文档中的三条注意点。
关系订阅:监听关系变更的 workaround
目前,关系更新的订阅只能通过UPDATED订阅配合 workaround 实现。
订阅关系变化
你可以通过触碰(touching)节点来强制触发变更通知:先在对应类型上添加一个dummy: String字段,然后在关系状态发生变化的节点上更新该字段:
mutation updatePost { updatePost( where: { id: "some-id" } data: { dummy: "dummy" # do a dummy change to trigger update subscription } ) }其原理是:关系本身的连接/断开不产生独立事件,但关系变化通常会伴随对该节点的一次写操作;通过更新一个无关紧要的dummy字段,就能让节点产生一次UPDATED事件,从而借道UPDATED订阅间接感知关系变化。若希望订阅直接支持关系触发器,可以关注该特性在社区中的讨论进展。
组合订阅:一次订阅多个变更类型
你可以在同一个订阅中订阅同一类型上的多种变更。
订阅所有节点上的所有变更
利用where对象的mutation_in参数选择要订阅的变更类型。例如同时订阅createPost、updatePost和deletePost对应的三种事件:
subscription { post(where: { mutation_in: [CREATED, UPDATED, DELETED] }) { mutation node { id description } updatedFields previousValues { description imageUrl } } }订阅特定节点上的所有变更
使用where对象的node参数圈定需要通知的特定节点,并与mutation_in组合。例如,仅当特定用户关注了作者时,才通知你该作者相关Post的创建、更新与删除:
subscription { post( where: { mutation_in: [CREATED, UPDATED, DELETED] } node: { author: { followedBy_some: { id: "cj03x3nacox6m0119755kmcm3" } } } ) { mutation node { id description } updatedFields previousValues { description imageUrl } } }注意:
previousValues对CREATED订阅始终为null;updatedFields对CREATED和DELETED订阅始终为null。
高级订阅过滤
你可以利用与查询相同的过滤系统,通过where参数实现更复杂的组合。例如,订阅所有CREATED和DELETED事件,外加imageUrl字段被更新时的所有UPDATED事件:
subscription { post(where: { OR: [{ mutation_in: [CREATED, DELETED] }, { mutation_in: [UPDATED] updatedFields_contains: "imageUrl" }] }) { mutation node { id description } updatedFields previousValues { description imageUrl } } }注意:在任何
updatedFields过滤条件与CREATED或DELETED订阅同时出现时,会返回错误。此外previousValues对CREATED始终为null,updatedFields对CREATED与DELETED始终为null。
订阅的底层实现:从数据库事件到 WebSocket 推送
理解了订阅 API 的用法后,再来看仓库中server/servers/subscriptions模块的实现,可以更深入地理解订阅事件的处理链路。
订阅管理器:按模型组织订阅
server/servers/subscriptions/src/main/scala/com/prisma/subscriptions/resolving/SubscriptionsManagerForModel.scala为每一个模型维护一个独立的订阅管理器 Actor:
- 启动时(
preStart),它会为模型的创建、更新、删除三条通道分别建立 pub/sub 订阅,并对连接断开(Terminated)与 Schema 失效(SchemaInvalidated)做出处理; - 收到
StartSubscription时登记订阅,收到EndSubscription时移除订阅; - 对同一条数据库事件,它会按“查询文本 + 变量”对订阅进行分组,同一组只执行一次过滤查询,再把结果广播给组内所有订阅者,从而显著降低数据库与计算开销(
runInChunksOf(maxParallelism = 10)控制并行度); - 当最后一个订阅者断开时,管理器会自动停止自身 Actor(
context.stop(self))。
事件通道:消息总线如何路由
server/servers/subscriptions/src/main/scala/com/prisma/subscriptions/resolving/MutationChannelUtil.scala定义了事件通道的命名规则:每个模型对应三条通道subscription:event:{projectId}:create{Model}、subscription:event:{projectId}:update{Model}、subscription:event:{projectId}:delete{Model}。服务器(servers/api与servers/deploy等模块)在数据变更时向对应通道发布事件消息,订阅服务通过消息总线(message-bus)消费这些消息并分发给对应模型的订阅管理器,管理器再根据通道名解析出变更类型(Created/Updated/Deleted)——这正是mutation_in语义的底层来源。
事件解析:35ms 延迟缓冲与 payload 组装
SubscriptionResolver.scala负责把数据库事件解析为订阅响应:
- 它根据
mutationType将事件 JSON 解析为DatabaseCreateEvent、DatabaseUpdateEvent或DatabaseDeleteEvent三种类型; - 出于读写分离一致性的考虑,生产环境从可能落后主库最多约 20ms 的从库读取数据,因此代码中人为加入了35ms 的缓冲延迟(源码注释明确提醒不要移除该延迟);
- 创建事件只携带
nodeId查询当前节点;更新事件携带previousValues与changedFields(即updatedFields与previousValues的 payload 来源);删除事件携带previousValues。这与前文“类型订阅”一节描述的 payload 语义完全一致。
协议层:与文档五步流程的对应
SubscriptionProtocol.scala中定义了两种协议:
SubscriptionProtocolV05(协议名graphql-subscriptions):即本文“原生 WebSockets”一节所使用的协议,消息类型包括init/init_success/init_fail/subscription_start/subscription_end/subscription_success/subscription_fail/subscription_data/keepalive,与文档中的五步交互一一对应;SubscriptionProtocolV07(协议名graphql-ws):更现代的订阅协议,消息类型为connection_init/connection_ack/connection_error/ka(keep-alive)/start/stop/data/error/complete。
测试印证
仓库在server/servers/subscriptions/src/test/scala/com/prisma/subscriptions/specs/目录下提供了大量订阅相关测试,例如:
SubscriptionFilterSpec.scala:验证UPDATED订阅中previousValues支持枚举类型、订阅支持别名(alias)等过滤行为;SubscriptionsProtocolV05Spec.scala与SubscriptionsProtocolV07Spec.scala:分别验证两种协议的完整消息交互流程;SubscriptionsManagerForModelSpec.scala:验证订阅管理器的启动、订阅登记与事件分发逻辑。
这些测试用例可以直接作为理解订阅 API 行为边界的参考:例如订阅查询可以使用 GraphQL 别名,枚举字段会出现在previousValues中,mutation_in支持UPDATED等取值。
小结
Prisma 1 的 GraphQL 订阅围绕CREATED/UPDATED/DELETED三种事件展开:每个对象类型自动生成类型订阅,配合mutation_in、node过滤与updatedFields系列过滤条件,可以精确订阅“某个类型的某个节点在某类变更时”的通知;订阅通过专用 WebSocket 端点(graphql-subscriptions或graphql-ws协议)传输,既可以借助 GraphQL Playground、Apollo Client 等现成工具,也可以基于原生 WebSocket 按五步流程自行实现。仓库中server/servers/subscriptions模块的实现则揭示了其底层架构:模型级订阅管理器、基于消息总线的create/update/delete{Model}事件通道、35ms 的一致性缓冲以及分组复用查询的优化策略。掌握这套 API 与实现原理,即可在基于 Prisma 1 的应用中构建实时通知、在线协作、实时看板等数据驱动场景。
【免费下载链接】prisma1💾 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL & MongoDB) [deprecated]项目地址: https://gitcode.com/gh_mirrors/pr/prisma1
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考