【免费下载链接】prisma1
💾 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL & MongoDB) [deprecated]
项目地址:
https://gitcode.com/gh_mirrors/pr/prisma1
点击查看 免费下载
本指南基于 Prisma 官方教程《Build a Realtime GraphQL Server with Subscriptions》(原文档位于 docs/1.10/03-Tutorials2/02-Build-GraphQL-Servers/01-Development/03-Build-a-Realtime-GraphQL-Server-with-Subscriptions.md),完整讲解如何借助 Prisma 的 Subscription API 与 Prisma bindings 的封装,在 GraphQL 应用层快速实现两类实时订阅(新文章创建/标题更新、文章删除)。读完本文,你将掌握 Prisma 自动生成的订阅 Schema(Subscription 类型、SubscriptionWhereInput、SubscriptionPayload)的完整字段语义、subscribe/resolve 两级订阅解析器的编写方法,以及基于 WebSocket 的长连接推送的底层原理。
目录
- Overview:为什么订阅是 GraphQL 实时能力的基石
- 1. 项目搭建
- 2. 理解 Prisma 的订阅 API
- 3. 添加 publications 订阅
- 4. 添加 postDeleted 订阅
- 5. 深入底层:仓库中的实现证据
- 总结
Overview:为什么订阅是 GraphQL 实时能力的基石
GraphQL 订阅(Subscriptions)最便捷的特性之一,是它与查询(Query)、变更(Mutation)使用完全相同的语法。对客户端而言,学习订阅几乎零成本。
订阅与查询/变更的本质区别在于执行模型:
- 查询和变更遵循典型的“请求-响应”循环,与普通 HTTP 请求无异——客户端发出请求,服务器计算并一次性返回结果;
- 订阅则不同:当 GraphQL 服务器收到订阅请求后,它并不会立即返回数据,而是与发起请求的客户端建立一条长连接(long-lived connection),客户端借此表达对某个事件(例如某个用户点赞了一张图片)的持续关注。
例如,下面这条订阅表达了“当 userId 对应的用户点赞图片时,向我推送图片的 URL 与用户名”:
subscription($userId: ID!) {
likeCreated(userId: $userId) {
user {
name
}
picture {
url
}
}
}
当事件真正发生时,服务器通过连接将请求的数据推送给订阅客户端:
{
"data": {
"likeCreated": {
"user": {
"name": "Alice"
},
"picture": {
"url": "https://media.giphy.com/media/5r5J4JD9miis/giphy.gif"
}
}
}
}
用 WebSocket 实现订阅
订阅通常基于 WebSocket 实现。除了实时逻辑(通常由 pub/sub 系统承担),你还需要实现 GraphQL 订阅的官方通信协议。只有当服务器严格遵循协议定义的流程时,客户端才能正确发起订阅请求并接收事件数据。
同时处理实时逻辑、pub/sub 系统、数据库访问以及订阅协议,复杂度会迅速上升;认证与授权逻辑还会进一步增加实现难度。此时,使用合适的抽象层会大幅简化工作——Prisma 与 Prisma bindings 的组合正是这样一层抽象:可以把它理解为一层“GraphQL ORM”,实时订阅开箱即用,让你轻松为 API 添加订阅能力。
1. 项目搭建
1.1 下载并探索 starter 项目
本教程配套一个 starter 项目(subscriptions 仓库)。使用以下命令下载并安装依赖:
curl https://codeload.github.com/nikolasburk/subscriptions/tar.gz/starter | tar -xz subscriptions-starter
cd subscriptions-starter
yarn install # or npm install
该项目包含一个非常简单的 GraphQL API,Schema 如下:
# import Post from "./generated/prisma.graphql"
type Query {
feed: [Post!]!
}
type Mutation {
writePost(title: String!): Post
updateTitle(id: ID!, newTitle: String!): Post
deletePost(id: ID!): Post
}
Post 类型由 Prisma 数据模型(database/datamodel.graphql)定义:
type Post {
id: ID! @unique
title: String!
}
本项目的目标是为 API 添加两条订阅:
- 一条订阅:当新的 Post 被创建,或已有 Post 的 title 被更新时触发;
- 一条订阅:当已有 Post 被删除时触发。
1.2 部署 Prisma 数据库 API
在启动服务器之前,需要先确保 Prisma 数据库 API 可用,且能被你的 GraphQL 服务器(通过 Prisma bindings)访问。
在 subscriptions-starter 目录下运行:
yarn prisma deploy
CLI 会询问一系列关于“如何部署 API”的问题。本教程选择 Demo server 选项,然后直接按 Enter 接受 service name 与 stage 的推荐值(如果你装有 Docker,也可以选择本地部署)。
部署完成后,CLI 会打印 Prisma 数据库 API 的 HTTP endpoint。复制该 endpoint,粘贴到 index.js 中实例化 GraphQLServer 的位置——注意需要用真实 endpoint 替换占位符 __PRISMA_ENDPOINT__。完成后代码大致如下:
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers,
context: req => ({
…req,
db: new Prisma({
typeDefs: 'src/generated/prisma.graphql',
endpoint: 'https://eu1.prisma.sh/jane-doe/subscriptions-example/dev',
secret: 'mysecret123',
debug: true,
}),
}),
})
1.3 打开 GraphQL Playground
运行 yarn dev 启动服务器并打开 GraphQL Playground。你可以自由探索项目,并发送一些查询与变更。
Note:Playground 会展示 .graphqlconfig.yml 中定义的两个 GraphQL API:app 项目代表应用层,由 /src/schema.graphql 定义;database 项目代表数据库层,由自动生成的 Prisma GraphQL Schema(/src/generated/prisma.graphql)定义。
2. 理解 Prisma 的订阅 API
2.1 概览
在动手实现订阅之前,先花一点时间理解 Prisma 提供的订阅 API——因为接下来正是要借力(piggyback)这个 API 来实现应用层的订阅。
一般来说,Prisma 允许你对数据模型中的每种类型订阅三类事件。以 Post 为例:
- 一个新的 Post 被创建
- 一个已有的 Post 被更新
- 一个已有的 Post 被删除
对应的 Subscription 类型定义如下(可在 /src/generated/prisma.graphql 中找到):
type Subscription {
post(where: PostSubscriptionWhereInput): PostSubscriptionPayload
}
如果不用 where 参数进一步约束,post 订阅会对上述三类事件全部触发。
2.2 过滤特定事件
where 参数允许客户端精确指定自己关心的事件。比如:某个客户端只想在 Post 被删除时收到通知;或者只想在 title 包含特定关键词的 Post 被创建时收到通知。这些约束都可以用 where 参数表达。
where 的类型定义如下:
input PostSubscriptionWhereInput {
# Filter for a specific mutation:
# CREATED, UPDATED, DELETED
mutation_in: [MutationType!]
# Filter for a specific field being updated
updatedFields_contains: String
updatedFields_contains_every: [String!]
updatedFields_contains_some: [String!]
# Filter for concrete values of the Post being mutated
node: PostWhereInput
# Combine several filter conditions
AND: [PostSubscriptionWhereInput!]
OR: [PostSubscriptionWhereInput!]
}
上面提到的两个例子,可以在 Prisma API 中用以下订阅表达:
# 仅在被_删除_的 Post 上触发
subscription {
post(where: {
mutation_in: [DELETED]
}) {
# … 选择集稍后讨论
}
}
# 仅在 title 包含 "GraphQL" 的 Post 被_创建_时触发
subscription {
post(where: {
mutation_in: [CREATED]
node: {
title_contains: "GraphQL"
}
}) {
# … 选择集稍后讨论
}
}
2.3 探索订阅的选择集
PostSubscriptionPayload 类型定义了 post 订阅中可以请求的字段:
type PostSubscriptionPayload {
mutation: MutationType!
node: Post
updatedFields: [String!]
previousValues: PostPreviousValues
}
2.3.1 mutation: MutationType!
MutationType 是一个包含三个值的枚举:
enum MutationType {
CREATED
UPDATED
DELETED
}
PostSubscriptionPayload 上的 mutation 字段因此携带了“发生了什么类型的变更”这一信息。
2.3.2 node: Post
该字段表示被创建、更新或删除的 Post 元素,允许你检索关于它的更多信息。
注意:对于 DELETED 变更,node 始终为 null。如果你需要了解被删除 Post 的更多细节,可以改用 previousValues 字段。
Note:GraphQL 中有时用 node 指代单个元素。一个 node 本质上对应数据库中的一条记录(record)。
2.3.3 updatedFields: [String!]
对于 UPDATED 变更,你可能关心的是“哪些字段被更新了”——这正是 updatedFields 的用途。
假设某客户端用以下订阅订阅了 Prisma API:
subscription {
post {
updatedFields
}
}
现在,假设服务器收到如下变更,更新某个 Post 的 title:
mutation {
updatePost(
where: {
id: "…"
}
data: {
title: "Prisma is the best way to build GraphQL servers"
}
) {
id
}
}
订阅客户端将收到如下载荷:
{
"data": {
"post": {
"updatedFields": ["title"]
}
}
}
这是因为该变更只更新了 Post 的 title 字段——没有别的。
2.3.4 previousValues: PostPreviousValues
PostPreviousValues 类型与 Post 本身非常相似:
type PostPreviousValues {
id: ID!
title: String!
}
它本质上是一个辅助类型,仅仅镜像 Post 的字段。
previousValues 只用于 UPDATED 和 DELETED 变更。对于 CREATED 变更,它始终为 null(原因与 DELETED 变更时 node 为 null 相同——事件发生时该记录还不存在)。
2.3.5 综合示例
再次考虑 2.3.3 节中的 updatePost 变更,但假设订阅查询包含上面讨论的所有字段:
subscription {
post {
mutation
updatedFields
node {
title
}
previousValues {
title
}
}
}
服务器执行上述变更后,推送给客户端的载荷如下:
{
"data": {
"post": {
"mutation": "UPDATED",
"updatedFields": ["title"],
"node": {
"title": "Prisma is the best way to build GraphQL servers",
},
"previousValues": {
"title": "GraphQL servers are best built with conventional ORMs",
}
}
}
}
注意,这里假设被更新的 Post 在执行变更前的 title 是 “GraphQL servers are best built with conventional ORMs”。
3. 添加 publications 订阅
掌握了 Prisma 的订阅 API 之后,就可以消费这套 API 来在应用层实现自己的订阅了。先从“新 Post 被创建或已有 Post 的 title 被更新”这条订阅开始。
3.1 扩展应用 Schema
第一步是扩展应用层的 GraphQL Schema,添加对应的订阅定义。
打开 schema.graphql,添加如下 Subscription 类型:
type Subscription {
publications: PostSubscriptionPayload
}
这里引用的 PostSubscriptionPayload 直接取自 Prisma GraphQL Schema,因此还需要在文件顶部导入它:
# import Post, PostSubscriptionPayload from "./generated/prisma.graphql"
Note:这种基于注释的导入语法由 graphql-import 包提供。截至目前,GraphQL SDL 还没有官方的跨文件类型导入方式。
3.2 实现订阅解析器
与查询、变更类似,添加新 API 功能的下一步是实现对应的解析器(resolver)。不过,订阅的解析器有些不同。
订阅解析器不再只是 Schema 定义中一个单一的解析函数,而是提供一个对象,其中至少包含一个名为 subscribe 的字段。subscribe 是一个函数,返回一个 AsyncIterator,用于为每个独立事件产出值。此外,还可以提供一个 resolve 字段——下一节会讨论它,现在先聚焦 subscribe。
更新 index.js 中的 resolvers 对象,加入 Subscription:
const resolvers = {
Query: {
// … 同前
},
Mutation: {
// … 同前
},
Subscription: {
publications: {
subscribe: (parent, args, ctx, info) => {
return ctx.db.subscription.post(
{
where: {
mutation_in: ['CREATED', 'UPDATED'],
},
},
info,
)
},
},
},
}
这里 Prisma bindings 帮你完成了繁重的工作:db.subscription.post(…) 返回的 AsyncIterator 会在 Post 类型上的每个事件发生时产出一个新值。
注意,这里特意过滤了 CREATED 和 UPDATED 变更,确保 publications 订阅只在这两类事件上触发。
3.3 测试订阅
测试订阅需要先启动服务器并打开 Playground——运行 yarn dev 即可。
在 Playground 中运行如下订阅:
subscription {
publications {
node {
id
title
}
}
}
Note:GraphQL Playground 有时会表现出一个已知 bug,即订阅直接返回 null 载荷。如果遇到这种情况,可以尝试该 issue 中提供的变通方案。
订阅运行后,响应面板会出现加载指示器,Play 按钮会变成红色 Stop 按钮,用于停止订阅。
此时可以再开一个标签页,发送变更来触发订阅:
mutation {
writePost(title: "GraphQL subscriptions are awesome") {
id
}
}
切回最初的标签页,你会看到订阅数据已经出现在响应面板中。还可以用 updateTitle 变更继续体验。
4. 添加 postDeleted 订阅
本节实现一个“每当 Post 被删除时触发”的订阅。流程与 publications 解析器大体相似,区别在于:这次将只返回被删除的 Post,而不是 PostSubscriptionPayload 类型的对象。
4.1 扩展应用 Schema
与往常一样,为 GraphQL API 添加新功能的第一步,是在 Schema 中把新操作表达为根字段(root field)。
打开 /src/schema.graphql,将 Subscription 类型调整为:
type Subscription {
publications: PostSubscriptionPayload
postDeleted: Post
}
对于 postDeleted,不再返回 PostSubscriptionPayload,而是直接返回被删除的 Post 对象。
4.2 实现订阅解析器
在 3.2 节中曾提到:实现订阅解析器的对象可以持有第二个函数 resolve(subscribe 是必须的)。本节将实际使用它。
下面是解析 postDeleted 订阅时 subscribe 与 resolve 的实现:
const resolvers = {
Query: {
// … 同前
},
Mutation: {
// … 同前
},
Subscription: {
publications: {
// … 同前
},
postDeleted: {
subscribe: (parent, args, ctx, info) => {
const selectionSet = `{ previousValues { id title } }`
return ctx.db.subscription.post(
{
where: {
mutation_in: ['DELETED'],
},
},
selectionSet,
)
},
resolve: (payload, args, context, info) => {
return payload ? payload.post.previousValues : payload
},
},
},
}
理解 subscribe 与 resolve 组合的关键在于:subscribe 返回的 AsyncIterator 产出的值,会作为 payload 参数传入 resolve!这意味着你可以用 resolve 按需转换和/或过滤 AsyncIterator 发出的事件数据。
注意,这里给 post 绑定函数传入的是一个硬编码的选择集,而不是像大多数情况那样传入 info 对象。因此,这次绑定函数的调用等价于向 Prisma API 发起如下订阅请求:
subscription {
post {
previousValues {
id
title
}
}
}
info 对象携带的是传入 GraphQL 操作(查询、变更、订阅皆然)的 AST(从而包含选择集)。但在这里,传入的选择集无法直接套用到 Prisma API 的 post 订阅上,原因如下:
- 传入订阅的返回类型只是 Post(如你在 schema.graphql 中所定义);
- Prisma GraphQL API 的 post 订阅返回类型是 PostSubscriptionPayload。
也就是说,传入的 info 对象与 post 订阅所需的结构不匹配,因此需要以字符串形式手动指定 post 订阅的选择集。
Note:这一点初看有些绕。如果一时难以理解,可以阅读这篇关于 info 对象及其在 GraphQL 解析器中作用的技术深潜文章。
坦白说,这种方式也并不完美:对于字段很多的类型,硬编码选择集会很快失控;另外,传入的订阅可能并不会请求类型的所有字段,此时就会发生过度获取(overfetching)。更优的方案是手动从 info 对象中提取被请求的字段,再传给 post 订阅(相关讨论见 graphql-binding 的 issue)。
无论如何,通过硬编码选择集,可以保证传入 resolve 的 payload 参数具有如下结构:
{
"post": {
"previousValues": {
"id": "…",
"title": "…",
}
}
}
正因如此,在 resolve 内部只需返回 payload.post.previousValues,得到的就是一个符合 Post 类型结构的对象。(注意:用三元运算符检查 payload 只是为了确保它不是 undefined,因为 undefined 可能破坏订阅。)
4.3 测试订阅
测试新订阅前需要重启服务器,确保改动生效:按 CTRL+C 终止服务器,再用 yarn dev 重启。
订阅运行后,发送如下变更(把 __POST_ID__ 占位符替换为数据库中实际 Post 的 id):
mutation {
deletePost(id: "__POST_ID__") {
id
}
}
切回订阅标签页,你会看到 id 与 title 已按活动订阅请求的内容被推送到响应面板。
5. 深入底层:仓库中的实现证据
应用层的这些 API 与行为,在当前仓库(Prisma 1.x)的源码中都能找到对应的实现证据。本教程展示的 Prisma bindings 客户端(db.subscription.post(…))与 Prisma 服务端的订阅子系统,构成了“应用层绑定 → WebSocket 协议 → pub/sub 事件 → 数据库事件解析 → 载荷回推”的完整链路。
5.1 客户端:订阅如何通过 WebSocket 传输
Prisma 客户端(Prisma bindings 所依赖的底层)在 cli/packages/prisma-client-lib/src/Client.ts 中实现了订阅的传输逻辑:
- 构造 SubscriptionClient 时,将 HTTP endpoint 替换为 WebSocket 协议:endpoint.replace(/^http/, 'ws'),并配置 lazy: true、reconnect: true 以及 60 秒的 inactivityTimeout(Client.ts 第 89-100 行);
- 当操作类型为 subscription 时,execute 通过 this._subscriptionClient.request({ query, variables }) 发起订阅,并把返回的可观察对象转换为 AsyncIterator(Client.ts 第 227-237 行)——这正是文档 3.2 节所说“db.subscription.post(…) 返回 AsyncIterator”的代码来源;
- 每个事件的载荷还会经过 mapSubscriptionPayload / extractPayload 处理:沿对象键逐层解包,剥去 { __typename } 之类的元数据,得到纯净的订阅载荷(Client.ts 第 164-225 行)。
可见,应用层解析器拿到的 AsyncIterator 和事件值,是 Prisma bindings 在客户端完成 WebSocket 通信、协议封装与载荷解包后的结果。
5.2 服务端:订阅子系统与消息通道
仓库的 Scala 服务端包含一个完整的订阅子系统(server/servers/subscriptions),用于支撑 Prisma API 的订阅能力:
- 协议层:SubscriptionProtocol 实现了两种 GraphQL 订阅通信协议——graphql-ws(v0.7,含 connection_init、start、data、complete 等消息类型)与 graphql-subscriptions(v0.5,含 init、subscription_start、subscription_data 等消息类型),见 protocol/SubscriptionProtocol.scala;
- 分发层:SubscriptionsManager 按项目维护订阅管理器,SubscriptionsManagerForModel 则针对每个数据模型管理活动订阅。它按“查询文本 + 变量”对相同订阅分组,只执行一次数据库事件处理并复用结果广播给多个订阅者(resolving/SubscriptionsManagerForModel.scala 第 117-158 行);
- 事件通道:MutationChannelUtil 为每个模型的创建/更新/删除分别构造频道名 createPost、updatePost、deletePost,并以 subscription:event:$projectId:$channel 的形式订阅 pub/sub 消息(resolving/MutationChannelUtil.scala);
- 载荷构造:SubscriptionResolver 将数据库事件解析为 DatabaseCreateEvent / DatabaseUpdateEvent / DatabaseDeleteEvent,其中更新事件携带 changedFields(对应 updatedFields)与 previousValues,删除事件携带删除前的 node 作为 previousValues 的数据来源(resolving/DatabaseEvents.scala 与 resolving/SubscriptionResolver.scala)。注意服务端对更新/删除事件构造 previousValues、对创建事件不构造,这与文档 2.3 节“previousValues 对 CREATED 恒为 null、node 对 DELETED 恒为 null”的语义完全对应。
5.3 Schema 生成器:订阅类型如何被自动生成
Prisma 的 GraphQL Schema 生成器(cli/packages/prisma-generate-schema)负责为每个数据模型生成订阅相关的 SDL 类型,这解释了 /src/generated/prisma.graphql 中 Subscription、PostSubscriptionPayload、PostSubscriptionWhereInput 的来源:
- subscriptionGenerator.ts 为每个模型生成形如 post(where: PostSubscriptionWhereInput): PostSubscriptionPayload 的根订阅字段(src/generator/default/subscription/subscriptionGenerator.ts);
- modelSubscriptionPayloadGenerator.ts 生成 mutation、node、updatedFields,并在模型含标量字段时附加 previousValues(src/generator/default/subscription/modelSubscriptionPayloadGenerator.ts);
- modelSubscriptionWhereInputGenerator.ts 生成 mutation_in、updatedFields_contains、updatedFields_contains_every、updatedFields_contains_some、node 以及 AND/OR/NOT 逻辑组合(src/generator/default/subscription/modelSubscriptionWhereInputGenerator.ts)。
生成结果的实测样例可以查看生成器自带的黑盒测试 fixture,例如 cli/packages/prisma-generate-schema/tests/blackbox/cases/defaultValue/document.graphql 中的 ASubscriptionWhereInput(第 255-277 行)与 ASubscriptionPayload(第 248-253 行),它们与文档中 PostSubscriptionWhereInput / PostSubscriptionPayload 的结构一一对应。
总结
在本教程中,你学会了如何使用 Prisma 与 Prisma bindings 为 GraphQL API 添加实时订阅能力。
与使用 Prisma 实现查询和变更类似,你是在“借力” Prisma 的 GraphQL API,把数据库访问与 pub/sub 逻辑的重活留给 Prisma 查询引擎承担。通过本文你应该掌握了:
- Prisma 订阅 API 的三类事件(CREATED / UPDATED / DELETED)与 SubscriptionWhereInput 的过滤语义(mutation_in、updatedFields_*、node、AND/OR);
- SubscriptionPayload 中 mutation、node、updatedFields、previousValues 四个字段各自的取值规则;
- 应用层订阅解析器中 subscribe(返回 AsyncIterator)与 resolve(转换/过滤事件载荷)的分工与配合;
- 订阅事件从数据库变更、pub/sub 频道到 WebSocket 推送的完整链路在仓库源码中的落地位置。
赞
【免费下载链接】prisma1
💾 Database Tools incl. ORM, Migrations and Admin UI (Postgres, MySQL & MongoDB) [deprecated]
项目地址:
https://gitcode.com/gh_mirrors/pr/prisma1
点击查看 免费下载
相关推荐
Mac运行Windows软件的终极优化方案:CXPatcher技术深度解析
MediaPipe背景分割终极指南:从模型选型到实战部署
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网硕互联帮助中心



评论前必须登录!
注册