无需 Prisma Client 的 Prisma:在 Next.js 中实现类型安全的 GraphQL 查询
学习如何使用 Prisma 处理模式定义,通过 prisma-kysely 生成 Kysely 类型,选择 Neon 或 pg 方言,以及如何在 Next.js 服务器代码中安全地进行类型查询。
来源: **Using Prisma for Schema and Kysely for Queries in a Next.js App**
Prisma 在模式设计方面表现优异,但如果你只是需要类型安全的 SQL 语句,将 Prisma Client 作为依赖项加入每个无服务器函数中未免过于臃肿。一种实用的划分方式是:在开发阶段由 Prisma 负责管理模式,通过生成器将其转换为 TypeScript 类型,而在生产环境中则由 Kysely 执行查询。本指南将展示如何在 Next.js App Router 项目中实现这种架构,并对比 Neon HTTP 方式与传统的 pg 数据池。
如果你希望采用传统方式,在运行时使用 Prisma Client,请参阅在 Next.js 16 项目中配置 Prisma 7。
各组件的职责划分
此架构中的每个工具都有明确的职能:
- Prisma 负责数据库设计:包括模式文件、数据库的同步或迁移,以及用于浏览数据的 Prisma Studio 等工具。
- Kysely 是运行时查询构建器。你可以在 TypeScript 中编写类似 SQL 的查询语句,编译器会检查表名和列名,而且该库本身的体积很小。
- prisma-kysely 则起到连接作用:它是一个 Prisma 生成器,能够直接根据你的 Prisma 模式文件生成 Kysely 类型定义。
Prisma 会融入你的开发者工作流程中,而 Kysely 是部署在代码中的唯一数据库层。相应的代价是:你将失去 Prisma Client 具备关系型功能的 API,比如嵌套的 include 操作、嵌套写入以及手动编写连接查询的功能。如果你仍在挑选数据层,关于原始 SQL、Prisma 和 Drizzle 的对比文章可以为你提供更全面的决策参考。
安装相关包
运行时需要 Kysely 以及对应的方言实现和驱动程序;这会安装基于 Neon 的技术栈(此处使用 pnpm,但 npm 或 yarn 也可):
pnpm add kysely kysely-neon @neondatabase/serverless
pnpm add -D prisma prisma-kysely
请注意,prisma 和 prisma-kysely 属于开发依赖项:它们仅负责管理数据架构并生成代码。
定义数据架构并生成 Kysely 类型
该架构仍是普通的 Prisma 架构,不同之处在于使用的生成工具是 prisma-kysely(将代码写入 ./types 目录),而非常规的客户端生成工具。它定义了 User 和 Post 两个实体,并建立了一对多的关联关系。
// db/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator kysely {
provider = "prisma-kysely"
output = "./types"
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
authorId Int
createdAt DateTime @default(now())
author User @relation(fields: [authorId], references: [id])
}
prisma generate 现在会生成 Kysely 类型而非客户端代码,而 prisma db push 则负责将该架构应用到数据库中:
pnpm prisma generate
pnpm prisma db push
生成的结果是一个类似 db/types/kysely.d.ts 的文件,其中包含描述每个表的 DB 接口。需要注意三点:
- 该架构文件位于
db/schema.prisma目录中,而非 Prisma 的默认路径prisma/schema.prisma,因此需根据所使用的 Prisma 版本通过--schema等参数指定该路径。
db push 可在不生成迁移文件的情况下同步数据库。对于生产环境数据,使用 prisma migrate dev 和 prisma migrate deploy 可以生成带版本号的迁移文件,同时不会改变其他工作流程。url 行被拒绝,請查看当前文档。日常操作用的包脚本
一些脚本能让团队更清晰地了解工作流程:
{
"scripts": {
"db:push": "prisma db push",
"db:generate": "prisma generate",
"db:studio": "prisma studio"
}
}
有了这些脚本,架构变更只需执行两条命令即可:
# To generate the types
pnpm db:generate
# To push to the remote DB
pnpm db:push
这样数据库与 Kysely 类型就能保持一致,编译器会标记出任何涉及已重命名或已被删除列的查询。
选择 Kysely 方言
Kysely 并不直接与数据库通信。在查询构建器与驱动程序之间存在一种方言,它知晓如何为特定的数据库和连接方式编译并发送查询。官方提供的方言包括 PostgreSQL、MySQL 和 SQLite;而社区开发的方言则支持 Neon、PlanetScale 和 Turso 等服务。对于 Postgres,有两种方案可满足大多数需求。
用于无服务器部署的 HTTP 版 Neon
在 Vercel 或类似平台上,Neon 的 HTTP 驱动程序非常适用:每个查询都是无状态请求,因此在频繁调用之间无需维持持久连接。配置时需将 neon() 包装在 NeonDialect 中,并使用生成的 DB 类型为 Kysely 添加类型定义:
// db/index.ts
import { Kysely } from 'kysely'
import { NeonDialect } from 'kysely-neon'
import { neon } from '@neondatabase/serverless'
import type { DB } from './types/kysely'
const db = new Kysely<DB>({
dialect: new NeonDialect({
neon: neon(process.env.DATABASE_URL!),
}),
})
export default db
!仅能让TypeScript静默运行,无法进行任何验证。更大的限制是,通过HTTP使用的无法执行事务或基于会话的查询,因此它更适合简单的CRUD操作和以读取为主的页面,而不适合需要多条语句处理的场景。使用Neon的WebSocket驱动时根本不需要:只需将Neon的Pool传递给Kysely内置的PostgresDialect即可。此外,的API在不同版本间也会发生变化,因此请参考其最新的README文档。
用于长时间运行的服务器的node-postgres
对于需要长期运行的Node.js服务器,官方的pg驱动是最完整的选择,它具备事务处理、连接池以及预编译语句功能。该驱动可直接与Kysely自身的PostgresDialect配合使用:
// db/index.ts
import { Kysely, PostgresDialect } from 'kysely'
import { Pool } from 'pg'
import type { DB } from './types/kysely'
const db = new Kysely<DB>({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: process.env.DATABASE_URL,
}),
}),
})
export default db
如何在这两者之间选择
两者的区别在于连接模型。kysely-neon 是无状态的 HTTP 方式:轻量且适合无服务器环境,但不支持事务处理。pg 则会保持 TCP 连接处于打开状态,并具备完整的功能集,适合持续运行的服务器。在 Vercel 或 Netlify 等无服务器平台上可使用 kysely-neon,而当需要事务处理时则可选择 pg 或 Neon WebSocket 连接池。需要注意的是,在无服务器函数中使用 pg 连接池时,每个实例都会单独建立连接,若没有外部连接池管理,很容易耗尽数据库的连接限制。
从服务器组件发起查询
任何服务器端代码都可以导入 db。这个服务器组件用于列出用户信息,按 id 顺序显示三列数据:
// app/users/page.tsx
import db from '@/db'
export default async function UsersPage() {
const users = await db
.selectFrom('user')
.select(['id', 'name', 'email'])
.orderBy('id')
.execute()
return (
<div>
<h1>Users</h1>
<ul>
{users.map(u => (
<li key={u.id}>{u.name} ({u.email})</li>
))}
</ul>
</div>
)
}
由于 DB 来自 Prisma 模型定义,如果表名或列名拼写错误就会导致类型检查失败,而 users 则会使用恰好选定的字段来生成。
有一个需要注意的地方:Prisma 默认会以模型名称作为表名,因此该模型定义会创建一个名为 User 的表,生成的类型也会使用这个名称。只有在使用 selectFrom('user') 时并在模型上添加 @@map("user"),才能进行类型检查;否则查询的表名仍为 'User'。另外,不要将 db 放入客户端组件中,因为它会读取仅服务器端可访问的密钥。
日常开发流程
当所有配置都设置完成后,操作流程如下:
- 在
db/schema.prisma中修改模型定义。 - 通过两个脚本重新生成类型并应用更改。
- 在服务器组件、路由处理程序或服务器动作中使用
db。
最终的架构将所有与数据库相关的内容都放在 db/ 目录下:
📦 my-next-app
├── 📁 app
│ └── 📁 users
│ └── page.tsx
├── 📁 db
│ ├── schema.prisma
│ ├── index.ts
│ └── 📁 types
│ └── kysely.d.ts
├── 📁 node_modules
├── .env
├── package.json
├── pnpm-lock.yaml
└── tsconfig.json
总结
这种分离方式让 Prisma 的模式定义流程与 Studio 保持独立,无需打包 Prisma Client,同时还能提供一个基于 SQL 结构的查询层,其类型会与模式定义保持一致。
- 将
prisma和prisma-kysely作为开发依赖项;只有 Kysely、某种方言以及驱动程序会被部署到生产环境。 - 对于简单的无服务器工作负载,可使用 Neon HTTP 方言;当需要事务处理时,则切换为带连接池的
PostgresDialect。 - 注意表名规则:生成的类型会跟随数据库名称,而数据库名称默认为 Prisma 模型的名称。
db push 转而使用正规的迁移工具。