使用 Prisma Bindings 从 Node 脚本访问 Prisma 服务:完整实战教程
使用 Prisma Bindings 从 Node 脚本访问 Prisma 服务:完整实战教程
本教程基于 Prisma 1.x 的官方操作手册,完整演示如何从一个独立的 Node 脚本中访问已经部署好的 Prisma 服务:通过更新数据模型、下载 Prisma 生成的 GraphQL Schema(Prisma database schema),再借助 prisma-binding 提供的 query、mutation、exists 与 request 四类能力向数据库发送查询与变更。读完本文,你将掌握从零搭建"Node 脚本 + Prisma Bindings"访问链路、自动同步 Schema 以及理解其底层实现原理的完整技能,并能直接套用到自己的脚本与微服务中。
教程背景与前置条件
本教程假设你已经拥有一个运行中的 Prisma 服务,并已拿到它的 endpoint(服务端点)。如果还没有自己的服务,可以先从仓库中的 Setup-Prisma 教程目录 选择一种方式快速搭建:
- 在 Demo 服务器上搭建 Prisma;
- 使用全新的 MySQL 数据库搭建;
- 使用全新的 Postgres 数据库搭建;
- 连接一个空的 MySQL 数据库搭建;
- 连接一个空的 Postgres 数据库搭建。
教程涉及的核心依赖有两个:prisma-binding(在 Node 中生成类型安全的数据库访问 API)与 graphql-cli(配合 GraphQL Config 下载 Prisma 的 GraphQL Schema)。从当前仓库的源码结构看,prisma-binding 的核心逻辑已被 prisma-client-lib 包 继承并演进,本文会在讲解每一步时同步给出对应的源码级佐证。
小提示:本教程中所有"必须执行"的操作都带步骤编号,如果你只想快速跑通、不关心背后的解释,可以直接按编号逐条执行。
Step 1:更新数据模型
首先保证已有 Prisma 服务的数据模型符合后续步骤的需要。这里假设数据模型存放在单个文件 datamodel.graphql 中(若你的目录结构不同,请自行调整)。
将 datamodel.graphql 的内容更新为如下两个模型,它们之间是一对多关系(一个 User 拥有多篇 Post,每篇 Post 有唯一的 author):
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 服务根目录(即 prisma.yml 所在的目录),执行以下命令更新服务的 GraphQL API:
prisma deploy
部署完成后,Prisma 服务的 GraphQL API 会自动为数据模型中的 User 和 Post 类型暴露完整的 CRUD 操作,并允许修改两者之间的关系(例如创建 Post 时通过 author.connect 关联作者)。这套 API 正是后续脚本中 prisma.query、prisma.mutation 等能力的基础——Prisma binding 实例会依据下载下来的 Schema 动态生成这些方法(详见 Client.ts 中的 getTypes 实现)。
Step 2:搭建项目目录结构
数据模型就绪后,开始搭建独立的 Node 脚本项目。新建一个目录并初始化项目:
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 与 graphql(后者用于解析 Schema 与选择集字符串):
yarn add prisma-binding graphql
Step 3:下载 Prisma 数据库 Schema
要让 Prisma binding 知道该生成哪些方法,需要先把 Prisma GraphQL API 的 Schema(即 Prisma database schema)下载到本地。下载工作由 GraphQL CLI 与 GraphQL Config 协作完成。
先全局安装 GraphQL CLI:
yarn global add graphql-cli
然后在 my-node-script 根目录创建 GraphQL Config 配置文件:
touch .graphqlconfig.yml
将以下内容写入其中,把 Prisma 的 GraphQL API 声明为一个名为 prisma 的 project:
projects:
prisma:
schemaPath: src/generated/prisma.graphql
extensions:
prisma: prisma/prisma.yml
这份配置文件同时被 GraphQL CLI 与 GraphQL Playground 使用。执行下面的命令,即可把 Prisma 数据库 Schema 下载到 src/generated/prisma.graphql:
graphql get-schema
下载完成后,定义数据库完整 CRUD API 的 Schema 就保存在 projects.prisma.schemaPath 指定的位置(本例为 src/generated/prisma.graphql)。仓库中的 getSchemaPathFromConfig.ts 展示了 CLI 侧如何解析这份配置:它会优先读取顶层 schemaPath,否则在 config.projects 中查找带 extensions.prisma 的 project 并取其 schemaPath——这与本教程 .graphqlconfig.yml 的写法完全对应。
让 Schema 随部署自动更新
如果希望每次 prisma deploy 后 Schema 都自动重新下载(例如数据模型有更新时),可以在 prisma.yml 中添加如下 post-deploy 钩子:
hooks:
post-deploy:
- graphql get-schema -p prisma
-p prisma 指定了 .graphqlconfig.yml 中的 project 名。这一机制在仓库的 deploy 命令实现 中可见:部署流程会读取 post-deploy 钩子列表并逐条执行(也可用 --skip-hooks 标志禁用)。
Step 4:使用 query 与 mutation 发送查询和变更
这一步将真正与 Prisma 服务通信。思路是:实例化一个 Prisma binding,把上一步下载的 Schema 指给它;同时提供 Prisma API 的 endpoint(存储在 prisma/prisma.yml 中)。binding 实例相当于 Prisma 服务的"JavaScript SDK",可以直接调用其 API 向数据库发送查询(query)与变更(mutation)。
将以下代码写入 src/index.js:
⚠️ 重要:务必将
__YOUR_PRISMA_ENDPOINT__替换为prisma/prisma.yml中保存的真实 Prisma endpoint。
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 }
运行脚本查看输出:
node src/index.js
这段脚本演示了 binding 的核心调用模式:每个方法都接收"参数对象 + 选择集字符串",例如 createUser({ data: { name: "Alice" } }, "{ id name }") 表示创建用户并只返回 id 和 name 字段;createPost 中的 author: { connect: { id } } 则是关系型写操作——把新帖子连接到已存在的用户;query.users(null, "{ id posts { title } }") 展示了嵌套选择集(同时取用户的 id 与关联帖子的 title)。
从源码角度解释这一 API 形态:在 Client.ts 中,客户端对外暴露 query、mutation、$subscribe、$graphql 与 $exists 五个能力;getTypes() 会基于 Schema 的类型地图为每个字段动态构建可链式调用的方法,把连续的方法调用累积成"指令"后统一编译为 GraphQL 文档(processInstructions),再交给底层的 BatchedGraphQLClient 批量发送;响应返回后由 extractPayload 剥掉顶层的操作名包装(Client.ts#L178-L225),所以你拿到的就是干净的 { id, name } 这样的数据。
Step 5:使用 exists 检查节点是否存在
除了 query 与 mutation,Prisma binding 还提供 exists 属性,用于判断数据库中是否存在满足某些属性的节点。exists 会为数据模型中的每个类型生成一个函数,函数名与类型同名——在本例中即 prisma.exists.User(filter) 与 prisma.exists.Post(filter)。这些函数接收 filter 参数,始终返回 true 或 false。
由于 Step 1 的数据模型包含 User 和 Post 两个模型,因此自动生成了 exists.User 和 exists.Post。
将 src/index.js 的内容替换为下面的代码:
⚠️ 重要:务必将
__YOUR_PRISMA_ENDPOINT__替换为prisma/prisma.yml中保存的真实 Prisma endpoint。
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
运行脚本查看输出:
node src/index.js
从实现上看,exists 并非额外调用某个特殊接口,而是"查询 + 判空"的封装:源码中的 buildExists 会遍历 Schema 的查询类型,为每个可查询类型生成 类型名小写(filter) 方法,内部等价于调用对应复数查询字段(如 users({ where: args })),然后通过 res.length > 0 判断是否存在;其类型签名在 types.ts 中定义为 (filter: Filter) => Promise<boolean>。因此 exists 的返回值永远是布尔值,非常适合作为脚本中的条件判断。
Step 6:使用 request 发送原始查询与变更
request 属性允许你把查询/变更写成完整字符串直接发送。相比 query/mutation 的链式调用,这种方式更冗长——需要手动拼写整个操作,且响应多一层开销:顶层会包含操作名作为 key。
request底层基于graphql-request,因此 API 与之一致。
将 src/index.js 的内容替换为下面的代码:
⚠️ 重要:务必将
__YOUR_PRISMA_ENDPOINT__替换为prisma/prisma.yml中保存的真实 Prisma endpoint。
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' } ] }
*/
运行脚本查看输出:
node src/index.js
对比两种方式的输出可以清楚看到差异:prisma.mutation.createUser(...) 直接返回 { id, name },而 prisma.request(mutation, variables) 返回的是 { createUser: { id, name } }——多包了一层操作名。这也解释了 Step 4 中 extractPayload 为什么要把顶层 key 剥掉:query/mutation 的链式 API 替你完成了这层解包,而 request 把原始响应原样交还给你。request 的第二个参数是变量对象,可在写死字符串的同时把动态参数(如上面的 $name)安全地传进去。
从 prisma-binding 到仓库实现:源码级延伸
如果你想深入理解这套机制的完整实现,当前仓库的 prisma-client-lib 包 是最佳入口,几个关键点值得关注:
- Client 基类:Client.ts 的构造函数接收
typeDefs、endpoint、secret、debug、models等选项。如果配置了secret,客户端会自动用该密钥签发 JWT,并在请求头携带Authorization: Bearer <token>(订阅连接则通过connectionParams携带),这就是生产环境鉴权接入的入口。 - 方法动态生成:
getTypes()从 Schema 类型地图出发,为每个根字段生成可链式调用的函数并内置 Promise 语义(then/catch),方法链会被累积成指令后统一编译执行——这是query/mutation链式写法的本质。 - 客户端工厂:makePrismaClientClass.ts 展示了
prisma generate生成客户端的方式:把typeDefs、endpoint、secret、models固化进一个继承Client的子类,之后无需再手动传参即可实例化——与本文手动new Prisma({ typeDefs, endpoint })的形态一脉相承。 - 批量请求与订阅:HTTP 侧使用
BatchedGraphQLClient(支持请求合并),订阅侧使用subscriptions-transport-ws的SubscriptionClient,并自动把http协议替换为ws。
小结
本教程完整走通了"Node 脚本访问 Prisma 服务"的五个步骤:更新数据模型并部署 → 搭建脚本项目目录 → 用 GraphQL CLI 下载 Prisma 数据库 Schema → 用 query/mutation 做增删改查 → 用 exists 判断节点存在性 → 用 request 发送原始 GraphQL 字符串。掌握了这套流程,你就可以在任何 Node 脚本、定时任务或后端服务中,以"JavaScript SDK"的方式安全、便捷地操作 Prisma 管理的数据库;若需要自动同步 Schema,别忘了配置 prisma.yml 中的 post-deploy 钩子。