Prisma ORM与TiDB Serverless设置指南
DataHot 速览
TiDB Cloud官方博客介绍如何在AI应用中用Prisma ORM连接TiDB。内容强调TiDB兼容MySQL,现有schema可无缝使用,并推荐通过@tidbcloud/prisma-adapter走HTTPS查询以满足serverless/edge环境,同时说明迁移和introspection仍需TCP连接。还针对AI应用的数据建模给出主键和索引设计建议。
为什么值得关注:面向数据从业者提供了Prisma连接TiDB的完整技术路径和serverless环境下的关键限制,对AI应用的数据建模有直接参考价值。
本文目录 14 节
译文
AI 逐段翻译关键要点
- TiDB 兼容 MySQL,因此 Prisma 可以通过
provider = "mysql"连接,且现有的 MySQL 模式无需更改即可使用。 - 使用
@tidbcloud/prisma-adapter通过 HTTPS 而非 TCP 进行查询——这对于无服务器函数和边缘运行时(Vercel Edge、Cloudflare Workers)是必需的,并可避免 TiDB Cloud 的连接限制。 - 适配器仅覆盖 Prisma Client 的查询。迁移、
db push和 introspection 仍需要标准的 TCP 连接,因此请在环境配置中保留两条连接路径。 - 当建模 AI 应用数据(会话、消息、工具调用、记忆)时,使用
uuid()或cuid(2)而非自增或cuid()作为主键以避免写入热点,并添加复合索引如[sessionId, createdAt]以便于聊天历史读取。
简介
Prisma 是大多数团队在 TypeScript 项目中使用的 ORM(对象关系映射器),它通过标准的 MySQL provider 连接到 TiDB,因此为 MySQL 编写的模式可以不加更改地使用。连接是工作的关键所在。无服务器函数和边缘运行时在每次调用时都会打开和丢弃连接,这是长期 TCP 连接池最不擅长的流量模式。
本指南涵盖安装适配器、配置环境、定义模式、运行查询和事务,以及建模 AI 应用持久化的数据。还介绍了适配器不支持的内容,因为迁移和 introspection 仍需要 TCP 连接。
虽然我们使用 TiDB Cloud Starter 来说明步骤,但这些步骤同样适用于 TiDB Cloud Essential。如果您使用 Dedicated、Premium 和 BYOC 层级,请遵循 此文档。
什么是 Prisma ORM?
Prisma 是一个开源的 Node.js 和 TypeScript ORM,从声明式模式文件生成类型安全的客户端。它支持 MySQL、PostgreSQL、SQLite 和 SQL Server。您在 schema.prisma 中一次描述模型,运行 generate 步骤,即可获得一个带有自动补全和编译时检查的客户端,针对您自己的表。
以下两点对后续内容很重要。
- Prisma Client 是生成的查询构建器。它针对您的模式进行类型化,因此拼写错误的字段会在构建时而不是在生产中失败。
- Prisma 查询引擎 将客户端调用转换为 SQL,然后将语句交给驱动程序执行。驱动适配器替换了该驱动程序,而保留转换步骤不变。这就是 Prisma 通过 HTTP 而非 TCP 套接字访问数据库的方式。
由于 TiDB 兼容 MySQL,Prisma 将其视为 MySQL。您的 provider 是 mysql,为 MySQL 编写的模式文件可以原样使用。

为什么 Prisma 是 Vibe Coding 技术栈的默认 ORM
Prisma 的模式是一个单一的声明式文件,语法可读,这使得它成为编码代理最可靠生成的格式。向 Cursor 或 ChatGPT 请求数据模型时,您更可能得到 schema.prisma 而不是原始的 DDL。当目标数据库兼容 MySQL 时,生成的模式可以直接运行。
通常的问题是方言。在大量 Postgres 示例语料上训练的模型会生成 Postgres 类型,您需要花费时间将 serial 和 jsonb 重写为 MySQL 可接受的类型。将 provider 切换为 mysql 是第一个更改,但不是唯一的更改。原生类型属性也必须随之更改。@db.Uuid、@db.JsonB 和 @db.Inet 在 MySQL 连接器上都会验证失败。有帮助的是这些失败发生的位置:prisma validate 会指出属性和其所在的行,因此您可以在任何内容到达数据库之前一次性修复它们。
generate 步骤也会检查模型的工作。如果代理发明了一个不成立的关系,prisma generate 和 TypeScript 编译器会在其到达查询之前捕获它。
为什么使用 TiDB Cloud Prisma Adapter?
@tidbcloud/prisma-adapter 包允许 Prisma Client 通过 HTTPS 而非长期 TCP 连接访问 TiDB Cloud。它位于客户端和 TiDB Cloud 无服务器驱动程序之间,从查询引擎获取 SQL 并通过 HTTP 发送。路径中没有连接池。
TiDB Cloud Starter 实例的三个限制使情况具体化。您可获得 400 个并发连接,或设置支出限额后 5000 个。连接若保持打开超过 30 分钟可能会被终止。此外,文档建议将连接生命周期限制在约 5 分钟。在 AWS 上,公共端点空闲超时为 340 秒。
在无服务器函数中,池化的 Prisma 设置会遇到所有三个问题。每次调用要么支付 TCP 握手费用,要么使用一个已经被关闭的池化连接。通过 HTTP,每个查询都是一个请求,因此没有需要调整大小的池,也不会在查询中途发现未使用的连接。
适配器也是从边缘运行时访问 TiDB 的唯一方式。Prisma Client 无法在 Vercel Edge Functions 或 Cloudflare Workers 中打开 TCP 套接字。
PingCAP 维护此适配器,Prisma 将其列在社区维护的驱动适配器中。错误和版本支持问题请前往该仓库的问题跟踪器,而不是 Prisma。

逐步使用 Prisma 设置 TiDB
六个步骤:安装包、设置 ES 模块类型、设置连接字符串、定义模式、push 并 generate、通过适配器实例化客户端。在免费的 Starter 实例上,这大约需要十分钟。
安装适配器
npm install @tidbcloud/[email protected] @tidbcloud/serverless dotenvnpm install [email protected] --save-devdotenv 是下面的查询脚本所必需的。在 pnpm 上显式安装它很重要,因为 pnpm 不会像 npm 那样提升传递依赖。
指定 ES 模块
这里的所有示例都使用 import 语法,因此 package.json 需要 ES 模块标志。没有它,运行查询脚本会失败并显示 Cannot use import statement outside a module:
{
"type": "module"
}配置环境
// .env
DATABASE_URL="mysql://username:password@host:4000/database?sslaccept=strict"如果您只使用 Prisma Client 而不使用迁移,则端口和 SSL 参数是不必要的,mysql://username:password@host/database 就足够了。
不要提交这个文件。在 Vercel 或 Cloudflare 上,将 DATABASE_URL 设置为平台环境变量。对于 Vercel,集成市场中的 TiDB Cloud 集成会为您生成连接变量,并且适用于 Starter 和 Essential 计划,这样就不必在预览和生产环境之间手动传递凭据。
此步骤也是让边缘部署工作的关键。Prisma Client 无法在 Vercel Edge Functions 或 Cloudflare Workers 中打开 TCP 套接字,因此 HTTPS 路径是您的查询能够在这些运行时中运行的原因。

TiDB Cloud 控制台中的连接对话框。
定义您的 schema:
// schema.prismagenerator client { provider = "prisma-client-js"}
datasource db { provider = "mysql" url = env("DATABASE_URL")}
model user { id Int @id @default(autoincrement()) email String? @unique(map: "uniq_email") @db.VarChar(255) name String? @db.VarChar(255)}然后同步 schema 并生成客户端:
npx prisma db push
npx prismageneratedb push 通过 TCP 连接,而不是通过适配器。这是两条连接路径第一次分叉,下面的兼容性部分将涵盖其余内容。
使用适配器运行和查询
// query.js
import { PrismaTiDBCloud } from '@tidbcloud/prisma-adapter';
import { PrismaClient } from '@prisma/client';
import dotenv from 'dotenv';
dotenv.config();
const connectionString = `${process.env.DATABASE_URL}`;
const adapter = new PrismaTiDBCloud({ url: connectionString });
const prisma = new PrismaClient({ adapter });
// insert
const user = await prisma.user.create({
data: {
email: '[email protected]',
name: 'test',
},
})
console.log(user)
// query
console.log(await prisma.user.findMany())
// delete
await prisma.user.delete({
where: { id: user.id },
})事务使用数组形式,其中操作要么全部成功,要么全部失败。
const createUser1 = prisma.user.create({
data: { email: '[email protected]', name: 'User One' },
})
const createUser2 = prisma.user.create({
data: { email: '[email protected]', name: 'User One duplicate' },
})
const createUser3 = prisma.user.create({
data: { email: '[email protected]', name: 'User Three' },
})
try {
// fails together, because email is unique
await prisma.$transaction([createUser1, createUser2])
} catch (e) {
console.log(e)
// succeeds together
await prisma.$transaction([createUser3], { isolationLevel: 'ReadCommitted' })
}交互式回调形式 prisma.$transaction(async (tx) => { … }) 也可以使用。适配器实现了 startTransaction,因此两种形式都可用。
注意隔离级别的值。Prisma 采用自己的 PascalCase 枚举 ReadCommitted,并在适配器看到它之前将其转换为 SQL 形式。传递“READ COMMITTED”不起作用。
需要设计考虑的一个限制:在 Starter 和 Essential 计划中,事务的最长持续时间为 30 分钟。对于请求范围的工作来说,这通常不是问题。它对于批量回填很重要,应该分批进行,而不是包装在单个事务中。
版本兼容性以及适配器未涵盖的内容
适配器仅涵盖 Prisma Client。迁移和内省通过 TCP 连接,这意味着一个可工作的设置通常需要两条路径都可用:HTTPS 用于运行时查询,TCP 用于 schema 更改。在环境配置中保留两者,否则 prisma db push 会因为仅客户端连接字符串而失败。
| 通过 HTTPS(适配器) | 通过 TCP(标准) | |
| Prisma Client 查询 | 是 | 是 |
| 事务,数组形式 | 是 | 是 |
| 事务,交互式形式 | 是 | 是 |
| prisma db push | 否 | 是 |
| prisma migrate | 否 | 是 |
| prisma db pull(内省) | 否 | 是 |
| Vercel Edge、Cloudflare Workers | 是 | 否 |
对部署的实际影响:schema 更改不能从边缘函数运行。请从 CI 或本地使用 TCP 连接字符串运行它们,并让部署的运行时仅使用 HTTPS 进行查询。
适配器版本与 Prisma 次要版本保持一致。首先选择您的 Prisma 版本,然后选择匹配次要版本中的最新适配器版本:
| 适配器 | Prisma Client | Serverless Driver |
| v6.17.x | v6.17.x | >=v0.1.0 |
| v6.12.x | v6.12.x | >=v0.1.0 |
| v6.6.x | v6.6.x | >=v0.1.0 |
使用 Cursor 或 ChatGPT 生成适用于 TiDB 的 Prisma Schema
有效的提示应指明数据库名称,说明其兼容 MySQL,列出实体和关系,并请求索引。缺少这四个元素,您将得到 Postgres 风格的 schema,然后需要手动翻译。
运行输出前需要检查的内容:
- 原生类型。@db.Uuid、@db.JsonB、@db.Inet 是 Postgres 类型,在 MySQL 连接器上验证会失败。请使用 String @db.VarChar(36)、Json 和 String @db.VarChar(45)。serial 变为 Int @default(autoincrement()),text[] 变为关系表。
- 主键,这一点是 TiDB 特有的。顺序键会将写入集中在一个区域。这包括 @default(autoincrement()) 和任何单调递增的索引,也包括 cuid(),它带有时间戳前缀,因此按创建顺序排序。生成的 schema 通常使用这两种。请使用 uuid() 或 cuid(2),它们是随机的,并且在 MySQL 上验证没问题。
- 索引。模型通常添加单列索引,而查询需要复合索引。仅靠 sessionId 上的索引无法满足 where sessionId = ? order by createdAt 的查询。
- 级联行为。onDelete 经常被省略。请有意决定,因为代理数据会累积,孤立行会占用存储配额。
- 显式 @db.Text。无界 String 在 MySQL 上映射为 VARCHAR(191),并且无需前缀长度即可索引,因此无需注意。问题是生成的 schema 在你想要索引的字段上写入了 @db.Text。将 @db.Text 保留给真正长的内容。
使用 Prisma 为 AI 应用数据建模:用户、会话、消息、工具调用、记忆
代理应用程序持久化一组可识别的实体,读取模式很窄。您更常获取一个会话按顺序排列的消息,或用户最近的会话,而不是在整个集合上运行分析。这应该驱动索引设计。
model User {
id String @id @default(uuid())
email String? @unique @db.VarChar(255)
createdAt DateTime @default(now())
sessions Session[]
}
model Session { id String @id @default(uuid()) userId String status String @db.VarChar(32) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
startedAt DateTime @default(now())
messages Message[]
memory MemoryObject[]
@@index([userId, startedAt])
}
model Message {
id String @id @default(uuid())
sessionId String
role String @db.VarChar(16)
content String @db.Text
createdAt DateTime @default(now())
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
toolCalls ToolCall[]
@@index([sessionId, createdAt])
}
model ToolCall {
id String @id @default(uuid())
messageId String
toolName String @db.VarChar(128)
arguments Json
result Json?
status String @db.VarChar(32)
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
@@index([messageId])
}
model MemoryObject {
id String @id @default(uuid())
sessionId String
kind String @db.VarChar(64)
payload Json
updatedAt DateTime @updatedAt
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
@@index([sessionId, kind])
}使用 uuid() 而不是 cuid() 或自增整数。顺序主键会将写入集中在 TiDB 的单个区域,而 cuid() 带有时间戳前缀,因此按创建顺序排序,行为类似。代理工作负载在这些表上恰好是写密集型的。
该 schema 中还有三处是经过深思熟虑的。
Message 上的 @@index([sessionId, createdAt]) 是支持聊天历史读取的复合索引。没有它,渲染对话将进行扫描和排序。
全表使用 onDelete: Cascade。代理数据增长迅速,会话会被删除。没有级联规则,您会积累孤立的工具调用,这些会占用存储。级联会贯穿整个深度。删除用户会删除其会话,每个会话会带走其消息、工具调用和记忆对象。如果身份存储在外部提供商(如 Clerk、Auth0 或 WorkOS)中,请删除 User 模型,并将 userId 保留为不透明字符串。在身份提供者中删除用户不会删除他们在 TiDB 中的数据,因此清理成为应用层级的任务,通常通过用户删除 webhook 实现。删除该用户在 TiDB 中的会话,现有的从 Session 到消息、工具调用和记忆对象的级联将处理其余部分。
为工具参数和结果使用 Json,而不是每个工具一个规范化列,因为工具 schema 比数据库更改更频繁。
将其保留在一个兼容MySQL的数据库中的理由是操作性的。会话状态、消息历史、工具结果和向量嵌入在单个请求路径中一起读取。将它们分散到文档存储、向量数据库和数据仓库中,会将一个查询变成三次网络调用,并使一致性成为你的问题。
Prisma还是Drizzle?TiDB两者都支持
两者都作为MySQL连接到TiDB,因此选择归结为你希望在代码和SQL之间有多少抽象,而不是兼容性。Prisma提供声明式模式、生成类型和迁移工作流。Drizzle更接近SQL,运行时更小,查询语法读起来就像它生成的语句。希望将模式作为单一事实来源的团队选择Prisma。希望看到SQL的团队选择Drizzle。
启动免费的TiDB Cloud Starter实例并连接Prisma
以上所有内容都在免费层运行。每个TiDB Cloud Starter实例包括每月免费配额:5 GiB行存储、5 GiB列存储和5000万请求单元。行存储是Prisma应用程序消耗的。在需要信用卡之前,每个组织还可以运行最多五个免费实例。
创建实例,复制连接字符串,并完成设置部分。如果适配器在某个Prisma版本上出现问题,请在GitHub仓库中提出问题。
这篇内容对你有用吗?
反馈只用于改善内容筛选,不等同于收藏