使用 Prisma Bindings 在 Node 脚本中访问 Prisma 服务:从数据模型、Schema 下载到查询与变更的完整实战
使用 Prisma Bindings 在 Node 脚本中访问 Prisma 服务:从数据模型、Schema 下载到查询与变更的完整实战
本教程面向已经拥有一个可运行 Prisma 服务(具备可用 endpoint)的开发者,讲解如何从零搭建一个 Node 脚本项目,借助 prisma-binding 以“自动生成的 SDK”方式访问 Prisma GraphQL API。你将掌握:如何更新 Prisma 数据模型并重新部署、如何用 GraphQL CLI 把 Prisma 数据库 Schema 下载到本地、如何实例化 Prisma binding 发送查询/变更,以及 exists 与 request 两个高级入口的用法。文中所有示例均可在 Node.js 环境中直接运行验证。
前置条件
本教程假设你已经有一个正在运行的 Prisma 服务,并且知道它的 endpoint(服务地址)。如果你还没有准备好自己的 Prisma 服务,可以参考仓库中以下 Setup 教程之一先完成搭建:
- 在 Demo Server 上设置 Prisma
- 使用新的 MySQL 数据库设置 Prisma
- 使用新的 Postgres 数据库设置 Prisma
- 连接你的空 MySQL 数据库设置 Prisma
- 连接你的空 Postgres 数据库设置 Prisma
本教程中所有“需要你亲自动手”的步骤都配有编号操作说明;如果你只想快速跑通而不关心背后的原理,可以直接按编号步骤顺序执行。
Step 1:更新数据模型
教程后续步骤依赖特定的数据模型,因此第一步先确保现有 Prisma 服务的数据模型符合要求。这里假设你的数据模型保存在单个文件 datamodel.graphql 中——如果不是,请先调整你的项目结构。
① 打开 datamodel.graphql,将内容更新为:
type User {
id: ID! @unique
name: String!
posts: [Post!]!
}
type Post {
id: ID! @unique
title: String!
content: String!
published: Boolean! @default(value: "false")
author: User!
}
这里用到两个 Prisma 数据模型中的关键指令:
@unique:标注该字段的值在数据库中唯一,id字段因此成为每个节点的稳定主键;@default(value: "false"):为published字段声明默认值,新建Post节点时无需显式传入。
同时,User.posts 与 Post.author 构成了 User ↔ Post 之间的双向关系。
② 保存文件后,打开终端,进入 Prisma 服务根目录(即 prisma.yml 所在目录),运行:
prisma deploy
prisma deploy 会把数据模型同步到 Prisma 服务并更新其 GraphQL API。此后,你的 Prisma 服务将为数据模型中的 User 与 Post 类型暴露完整的 CRUD 操作,并允许修改两者之间的关系(relation)。
从仓库源码可以看到,deploy 命令在完成迁移后会执行 post-deploy 钩子(见 cli/packages/prisma-cli-core/src/commands/deploy/deploy.ts#L311-L343),这正是我们后面用来自动刷新本地 Schema 的机制。
Step 2:搭建项目目录
数据模型更新完成后,开始搭建脚本项目的目录结构。
① 新建一个目录,并在终端中粘贴以下命令:
mkdir -p my-node-script/src
touch my-node-script/src/index.js
cd my-node-script
yarn init -y
② 接下来,把 Prisma 服务的根目录移动到 my-node-script 内并重命名为 prisma:
cd ..
mkdir my-node-script/prisma
mv datamodel.graphql prisma.yml my-node-script/prisma
cd my-node-script
此时项目的目录结构如下:
.
└── my-node-script
├── package.json
├── prisma
│ ├── datamodel.graphql
│ └── prisma.yml
└── src
└── index.js
③ 安装 prisma-binding:
yarn add prisma-binding graphql
prisma-binding 是专为 Prisma GraphQL API 设计的 GraphQL binding,可以把它理解成 Prisma 服务的“自动生成 SDK”:你不需要手写 SQL 或直接访问数据库 API,大部分数据库操作在 resolver 里都是一行调用。graphql 是它解析 Schema 所依赖的运行时库。
Step 3:下载 Prisma 数据库 Schema
下一步要把 Prisma GraphQL API 的 GraphQL Schema(即 Prisma database schema,描述了你数据库的完整 CRUD API)下载到本地项目中,供 Prisma binding 指向使用。下载动作由 GraphQL CLI 和 GraphQL Config 配合完成。
① 全局安装 GraphQL CLI:
yarn global add graphql-cli
② 在项目根目录(my-node-script)创建 .graphqlconfig:
touch .graphqlconfig.yml
③ 向其中写入以下内容,把 Prisma 的 GraphQL API 定义为一个 project:
projects:
prisma:
schemaPath: src/generated/prisma.graphql
extensions:
prisma: prisma/prisma.yml
这个文件提供的信息同时被 GraphQL CLI 和 GraphQL Playground 使用:
projects.prisma.schemaPath:声明 Prisma 数据库 Schema 下载后要保存到的位置;projects.prisma.extensions.prisma:指向prisma.yml,让 GraphQL CLI 知道如何连接你的 Prisma 服务(endpoint 等连接信息都从那里读取)。
④ 运行以下命令,把 Prisma 数据库 Schema 下载到 src/generated/prisma.graphql:
graphql get-schema
命令执行完毕后,定义了数据库完整 CRUD API 的 Prisma 数据库 Schema 就会出现在 projects.prisma.schemaPath 指定的位置(即 src/generated/prisma.graphql)。
💡 进阶技巧:如果你希望每次向 Prisma 服务部署变更(例如更新数据模型)后 Schema 都自动刷新,可以在
prisma.yml中添加一个post-deploy钩子(钩子说明见 prisma.yml YAML 结构):
hooks:
post-deploy:
- graphql get-schema -p prisma
关于该钩子的底层执行:deploy 命令会在迁移成功后读取 post-deploy 钩子列表,并通过 spawnSync 逐条执行其中的终端命令(见 deploy.ts#L311-L343);钩子的读取逻辑位于 cli/packages/prisma-yml/src/PrismaDefinition.ts#L385-L394,它支持把单个字符串或字符串数组统一规范化为命令列表。post-deploy 是 Prisma CLI 目前提供的钩子类型,除了刷新 Schema,也可以用来在部署后执行 echo、代码生成等其他终端命令(见 prisma.yml YAML 结构中的示例)。
Step 4:发送查询与变更
现在进入核心环节——用 Prisma binding 与 Prisma 服务通信。
首先,通过指向上一步下载的 Schema 来实例化一个 Prisma binding。binding 实例需要连接你的 Prisma API,因此还要提供 endpoint(它存储在 prisma/prisma.yml 中)。这个 binding 实例相当于你 Prisma 服务的 JavaScript SDK,可以借助它的 API 向 Prisma 数据库发送查询和变更。
① 在 src/index.js 中写入以下代码。它创建了一个 Prisma binding 实例,并用它向 Prisma 服务发送查询和变更:
⚠️ 重要:请务必将
__YOUR_PRISMA_ENDPOINT__替换为你的 Prisma endpoint(即prisma/prisma.yml中存储的值)。
const { Prisma } = require("prisma-binding")
const prisma = new Prisma({
typeDefs: "src/generated/prisma.graphql",
endpoint: "__YOUR_PRISMA_ENDPOINT__"
})
prisma.mutation
.createUser({ data: { name: "Alice" } }, "{ id name }")
.then(console.log)
// { id: 'cjhcidn31c88i0b62zp4tdemt', name: 'Alice' }
.then(() => prisma.query.users(null, "{ id name }"))
.then(response => {
console.log(response)
// [ { id: 'cjhcidn31c88i0b62zp4tdemt', name: 'Alice' } ]
return prisma.mutation.createPost({
data: {
title: "Prisma rocks!",
content: "Prisma rocks!",
author: {
connect: {
id: response[0].id
}
}
}
})
})
.then(response => {
console.log(response)
/*
{ id: 'cjhcidoo5c8af0b62kv4dtv3c',
title: 'Prisma rocks!',
content: 'Prisma rocks!',
published: false }
*/
return prisma.mutation.updatePost({
where: { id: response.id },
data: { published: true }
})
})
.then(console.log)
/*
{ id: 'cjhcidoo5c8af0b62kv4dtv3c',
title: 'Prisma rocks!',
content: 'Prisma rocks!',
published: true }
*/
.then(() => prisma.query.users(null, "{ id posts { title } }"))
.then(console.log)
// [ { id: 'cjhcidn31c88i0b62zp4tdemt', posts: [ [Object] ] } ]
.then(() => prisma.mutation.deleteManyPosts())
.then(console.log)
// { count: 1 }
.then(() => prisma.mutation.deleteManyUsers())
.then(console.log)
// { count: 1 }
这段代码串起了一条完整的“创建 → 查询 → 关联 → 更新 → 级联查询 → 清理”链路:
prisma.mutation.createUser创建名为Alice的用户,第二个参数"{ id name }"是选择集(selection set),用来指定返回哪些字段;prisma.query.users查询所有用户,注意参数是(null, ...)——第一个参数为null表示不传过滤条件;prisma.mutation.createPost创建一篇帖子,并通过author: { connect: { id: ... } }把帖子关联到已存在的用户节点,这是 Prisma 中连接关系(relation connect)的标准写法;prisma.mutation.updatePost通过where: { id }定位节点并更新published字段;prisma.mutation.deleteManyPosts/deleteManyUsers批量删除,返回值是{ count }。
② 运行脚本,在终端中查看查询结果:
node src/index.js
关于 Prisma 实例构造参数的完整说明,可以参考仓库中的 Prisma Bindings API 参考:核心参数包括 typeDefs(Schema 文件路径,必填)、endpoint(服务地址,必填)、secret(服务密钥,可选)、debug(默认 false,设为 true 可将所有查询/变更打印到控制台)等。从仓库源码看,与 binding 同理念的客户端实现(cli/packages/prisma-client-lib/src/Client.ts)在内部通过 http-link-dataloader 提供的 BatchedGraphQLClient 统一发送请求(见 Client.ts#L20 与 this._client.request(query, variables) 调用点),因此这类客户端天然具备请求批处理能力——这也是“每个 delegate 函数调用最终都被翻译成一次 HTTP 请求”这一设计(见 Prisma Bindings API)的实现基础。
Step 5:用 exists 检查节点是否存在
除了 query 和 mutation,Prisma binding 还提供了一个非常方便的 exists 属性,用来检查数据库中是否存在满足特定条件的节点。exists 为数据模型中的每个类型暴露一个函数,函数名与类型同名(在本例中就是 prisma.exists.User(filter) 和 prisma.exists.Post(filter))。这些函数接收 filter 参数,并始终返回 true 或 false。
因为 Step 1 创建的数据模型包含 User 和 Post 两个模型,所以 Prisma 为你生成了 exists.User 和 exists.Post。
① 在 src/index.js 中写入以下代码:
⚠️ 重要:请务必将
__YOUR_PRISMA_ENDPOINT__替换为你的 Prisma endpoint(即prisma/prisma.yml中存储的值)。
const { Prisma } = require("prisma-binding")
const prisma = new Prisma({
typeDefs: "src/generated/prisma.graphql",
endpoint: "__YOUR_PRISMA_ENDPOINT__"
})
prisma.mutation
.createUser({ data: { name: "Alice" } }, "{ id name }")
.then(response => {
return prisma.mutation.createPost({
data: {
title: "Prisma rocks!",
content: "Prisma rocks!",
author: {
connect: {
id: response.id
}
}
}
})
})
.then(() => prisma.exists.User({ name: "Alice" }))
.then(response => console.log(response))
// true
.then(() => prisma.exists.Post({ title: "Prisma rocks" }))
.then(response => console.log(response))
// true
.then(() => prisma.mutation.deleteManyPosts())
.then(() => prisma.mutation.deleteManyUsers())
.then(() => prisma.exists.Post({ title: "Prisma rocks" }))
.then(response => console.log(response))
// false
.then(() => prisma.exists.User({ name: "Alice" }))
.then(console.log)
// false
这段脚本的验证路径非常清晰:
- 创建用户
Alice及其关联帖子后,exists.User({ name: "Alice" })与exists.Post({ title: "Prisma rocks" })均返回true; - 批量删除全部帖子与用户后,同样的两个
exists调用都返回false。
exists 非常适合用于“去重校验”“数据存在性前置判断”等场景。值得留意的是,exists 的过滤对象可以嵌套——例如 Prisma Bindings API 参考 中展示了 prisma.exists.Post({ id: 'abc', author: { name: 'Sarah' } }) 这种同时校验帖子自身属性及其关联作者属性的写法。
② 运行脚本,在终端中查看查询结果:
node src/index.js
Step 6:发送原始查询与变更
Prisma binding 还允许通过 request 属性,以字符串形式向 Prisma 服务发送查询和变更。这种方式更“啰嗦”——你必须完整拼写出整个查询/变更;同时响应也有少量额外开销,因为响应会包含查询/变更的名称作为顶层键(top level key)。
request底层使用了graphql-request,因此 API 与它保持一致。
① 用以下代码替换 src/index.js 的内容:
⚠️ 重要:请务必将
__YOUR_PRISMA_ENDPOINT__替换为你的 Prisma endpoint(即prisma/prisma.yml中存储的值)。
const { Prisma } = require("prisma-binding")
const prisma = new Prisma({
typeDefs: "src/generated/prisma.graphql",
endpoint: "__YOUR_PRISMA_ENDPOINT__"
})
const query = `
{
users {
id
name
}
}
`
const mutation = `
mutation CreateUser($name: String!) {
createUser(data: { name: $name }) {
id
name
}
}
`
const variables = { name: 'Bob' }
prisma.mutation
.createUser({ data: { name: 'Alice' } }, '{ id name }')
.then(console.log)
// { id: 'cjhcijh30cgww0b622rwkkvbo', name: 'Alice' }
.then(() => prisma.request(mutation, variables))
.then(console.log)
// { createUser: { id: 'cjhcijjndch0d0b62qux6o52a', name: 'Bob' } }
.then(() => prisma.query.users(null, '{ id name }'))
.then(console.log)
/*
[ { id: 'cjhciiacxcf850b62mcuaa3uz', name: 'Alice' },
{ id: 'cjhcijjndch0d0b62qux6o52a', name: 'Bob' } ]
*/
.then(() => prisma.request(query))
.then(console.log)
/*
{ users:
[ { id: 'cjhciiacxcf850b62mcuaa3uz', name: 'Alice' },
{ id: 'cjhcijjndch0d0b62qux6o52a', name: 'Bob' } ] }
*/
观察输出可以清楚看到 request 与 delegate 函数的差异:
prisma.mutation.createUser({ data: { name: 'Alice' } }, '{ id name }')的返回是扁平的节点对象{ id, name };- 而
prisma.request(mutation, variables)的返回多了一层操作名:{ createUser: { id, name } }。这里mutation字符串中定义了名为CreateUser的 mutation 和$name变量,variables对象则负责提供变量的实际值; prisma.request(query)同理,返回{ users: [ ... ] }而不是裸数组。
② 运行脚本,在终端中查看查询结果:
node src/index.js
更多关于 request 的用法示例(包括如何通过 prisma.request(query, variables) 传入变量并接收 {"data": {...}} 形式的响应)可以查阅 Prisma Bindings API 参考 中的 request 一节。
总结
通过本教程,你完成了一条完整的“从脚本访问 Prisma 服务”工作流:
| 环节 | 关键动作 | 产物 |
|---|---|---|
| 数据模型 | 编辑 datamodel.graphql + prisma deploy |
包含 User/Post CRUD 与关系的 GraphQL API |
| 项目搭建 | yarn init -y + yarn add prisma-binding graphql |
可运行的 Node 脚本项目 |
| Schema 下载 | 配置 .graphqlconfig.yml + graphql get-schema |
本地 src/generated/prisma.graphql |
| 编程访问 | prisma.query / prisma.mutation |
类型化的查询/变更 delegate 函数 |
| 存在性检查 | prisma.exists.User(filter) |
true/false 布尔结果 |
| 原始请求 | prisma.request(query, variables) |
兼容 graphql-request 的底层能力 |
核心要点回顾:
query与mutation是自动生成的 delegate resolver,(args, info)两参形式中args传操作参数、info传选择集字符串(详见 API 参考);exists按类型生成存在性检查函数,可嵌套过滤条件;request以原始字符串方式发送查询/变更,与graphql-request同 API,响应带操作名顶层键;- 若希望在每次
prisma deploy后自动刷新本地 Schema,可在prisma.yml中配置post-deploy钩子(钩子配置说明)。
现在,你完全可以在自己的 Node 脚本、定时任务或 GraphQL server 的 resolver 中,用这种“binding 即 SDK”的方式高效访问 Prisma 服务了。更系统的概念讲解(为什么用 binding 做 resolver 委托)可继续阅读 Prisma Bindings 概览。