惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
D
DataBreaches.Net
F
Fortinet All Blogs
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Apple Machine Learning Research
Apple Machine Learning Research
H
Help Net Security
M
MIT News - Artificial intelligence
美团技术团队
人人都是产品经理
人人都是产品经理
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Cloudflare Blog
有赞技术团队
有赞技术团队
L
LangChain Blog
博客园 - Franky
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 【当耐特】
S
SegmentFault 最新的问题
V
Visual Studio Blog
Blog — PlanetScale
Blog — PlanetScale
Hugging Face - Blog
Hugging Face - Blog
B
Blog
I
InfoQ

定の栈

AI 写得出功能,写不好 Dockerfile:Next.js 自托管踩坑记录 多个 AI 一起写代码时,我给它们划的边界 9 个 commit,没有一个人类作者:AI 独自写完一个项目之后 Next.js 渲染模式排查笔记:force-dynamic、hydration 与构建期没有数据库 我的 CLAUDE.md 只有一行:约定文件到底该写多少 同一个月,把主页搬出 Next.js,又把 Hugo 站搬进来 macOS Tahoe 如何用自己的视频做动态壁纸(替换 Aerial 方法) Podman Compose 常用命令速查 创建 Swap 文件 Cursor 2025 Hugo 中文阅读时间计算模版 手动重加载不蒜子计数 入坑舞萌 DX 历时两月终抵 w0 与首鸟加 多邻国 600 天连胜 原神五周年纪念 Asuna 成年生日 告别绝区零 站长18周岁啦! 一位动漫迷的追番日志与热情之旅 起飞日志 Reflector 镜像列表更新常用命令与配置文件 个人自用 rsync 文件同步常用命令 历时千日原神,深渊终抵满星 Arch Linux 个人常用命令记录 《三体III:死神永生》 《三体II:黑暗森林》 《千恋*万花》与现代物理学概念奇妙碰撞后产生出的糟糕想法 Minecraft 15 周年骨折价补票入正 网页添加 Live2D 看板娘 记第二次清醒控梦体验
让 Claude 少写错数据层:Prisma、Drizzle 和几条结构化做法
Asuna · 2026-07-21 · via 定の栈

最近几个项目分别用了 Prisma、Drizzle 和干脆不用 ORM 直接写 SQL。

同一个 AI 在这三种上犯的错完全不一样,这点挺出乎我意料的… Prisma 上它写的是「上个大版本的正确写法」,Drizzle 上它卡在类型和 API 签名,裸 SQL 上它压根不知道那个 driver 有什么限制。

这里记录下我从 schema 到迁移这条链路上用来兜底的几条做法,纯属备忘。

Prisma 7 / Drizzle 0.45 / Next 16。版本对不上的话,下面这些写法都会变哦。

Prisma 7 有几个 AI 一定会写错的地方

mikiacg 那个仓库的 CLAUDE.md:18-25 有一节直接叫「关键版本差异(AI 常见错误)」,六条 bullet 里跟 Prisma 相关的就一条:

Prisma 7 generator 为 "prisma-client"(不是 "prisma-client-js"),导入路径 @/generated/prisma/client

一条塞了两件事。同一个仓库的 .cursor/rules/library-versions.mdc 结尾那张 ❌→✅ 对照表把它拆成了第 3、4 两项,分开又写了一遍(那个仓库同时维护着三套约定文件,这么写的代价我在讲约定文件那篇里算过账,这里只挑跟数据层有关的看)。

看起来冗余,其实很有必要。因为这两件事不是「AI 不会」,是「AI 记的是旧的」。

不写死它百分之百按记忆猜,屡试不爽捏。

我自己再补一条:Prisma 7 起连接串从 schema 挪到了 prisma.config.ts,而那个文件里的校验通常写成无条件抛错。

vns-next 的 prisma.config.ts:5-8 就是这样:

const url = process.env.DATABASE_URL;
if (!url) {
  throw new Error("DATABASE_URL is not defined");
}

后果是 Docker 构建期必须喂一个占位值,哪怕 prisma generate 根本不连库——镜像里那两行 SKIP_ENV_VALIDATION=1 加假连接串就是这么来的,我在讲自托管那篇里贴过原文。

要留意的是这个因果方向:不是 Dockerfile 写错了,是 prisma.config.ts 里那句无条件 throw 把构建期一起管上了。校验写在配置文件顶层的库都有这毛病,Prisma 只是我最近撞上的那个。

这几条全属于「AI 训练数据里是旧写法」,写进 CLAUDE.md 是性价比最高的一类约定,一行字省你半小时喔。

db push 建不出原生 SQL 索引

这个坑不怪 AI,但 AI 也不会主动提醒你。

vns-next 的搜索用了 pg_trgm 扩展加三个 GIN 索引:标题模糊、别名数组包含、标签名模糊。这些 prisma db push 一个都不会帮你建,它只同步 schema 里表达得出来的东西。

所以我把它们单独放了一份 scripts/db/indexes.sql,文件头第一行注释就写着「Prisma schema 无法声明 trgm/表达式索引,db:push 后手动执行」。

更要紧的是验收方式。commit 35659c4 的 body 里记的那句是「pg_trgm 搜索(EXPLAIN 验证走索引)」。

查询能跑通不代表走了索引,全表扫也能返回正确结果,只是慢。这种事必须看执行计划,不能看接口有没有 200 啦。

Drizzle 侧的几个摩擦点

thdl 那边用的是 Drizzle + postgres.js,摩擦点完全换了一批。

连接池

dev 热重载会把连接一次次新建、泄漏光,得挂 globalThis 单例。

这是 Prisma 用户很熟的坑,换到 Drizzle 一样要手写一遍(src/lib/db/index.ts:8-13):

declare global {
  var __thdl_pg: ReturnType<typeof postgres> | undefined;
}

const client = globalThis.__thdl_pg ?? postgres(connectionString, { max: 10 });
if (process.env.NODE_ENV !== "production") globalThis.__thdl_pg = client;

枚举列遇上 searchParams

URL 上来的永远是 string,而 pgEnum 那一列是 9 个字面量的联合,中间没有干净的转换路径。

最后代码里写成了这样(src/app/(site)/resources/page.tsx:21):

cat ? eq(resources.category, cat as "music") : undefined

随便挑一个成员做断言。能跑,但它把校验责任整个丢掉了,真要认真做就得先过一遍 zod 的 enum。

这里我先记着,还没动…

又是版本差异

Drizzle 的表定义第二个参数从 0.36 起就改成了返回数组,不再是返回对象。thdl 里装的是 0.45.2,写法见 src/lib/db/schema.ts:113-117

(t) => [
  index("resources_status_created_idx").on(t.status, t.createdAt),
  index("resources_category_idx").on(t.category),
]

AI 大概率给你写成 => ({ ... }),跟 Prisma 那两条一个性质呢。查 changelog 的时候记得别翻错版本,0.45 里找不到这条的。

还有个不是代码的问题

本地我跑的是 pnpm db:push,README 里给别人的步骤同样是 pnpm db:pushdrizzle/ 目录下到现在就一个 baseline migration。

package.json:14 里确实定义了 db:migrate,但全仓库 grep 不到任何地方调用它。部署侧更干脆,deploy/ 里只有一个 Caddyfile 和一个 systemd unit,ExecStart 就是 node server.js,全程不碰数据库。

也就是说,schema 是怎么一步步演进过来的,没有任何版本化记录,全靠 push 当场对齐。

现在库还小,push 一把梭确实没事。等表结构复杂起来、或者真有第二个人要部署一份,这笔账是要还的… 我心里有数,就是还没动。

ORM 和认证库的接线要手工对齐

thdl 用的是 better-auth 的 drizzleAdapter,它约定了 user / session / account / verification 四张表。

但你自己 schema 里的表名不一定叫这个,得手工映射一遍(src/lib/auth.ts:8-16):

database: drizzleAdapter(db, {
  provider: "pg",
  schema: {
    user: schema.users,
    session: schema.sessions,
    account: schema.accounts,
    verification: schema.verifications,
  },
}),

这段 AI 写不对不是因为笨,是因为它没法凭空知道你的表叫什么。这种就属于必须人先定死、再让它接线的部分啦。

顺带一个化石:那个仓库 .claude/settings.local.json 的白名单里连着两条命令,删掉旧认证库的 catch-all 路由目录、新建新认证库的 catch-all 路由目录。

一次换库的过程就这么被完整记录下来了,而最终那个 route 文件只有 4 行,笑死。

迁移脚本要幂等,而且要配单测

vns-next 从 Hugo 老站迁到 Postgres,scripts/migrate-content.ts 的文件头注释第一句就是策略:「幂等可重跑:Work/Page 按主键 upsert;DownloadLink 先 deleteMany 再插;SiteLink/TeamMember/DlEntry 先清后插。」

中途挂了直接再跑一遍就行,这个前提让整个迁移过程轻松太多。

解析逻辑我抽成了独立模块 scripts/lib/parse.ts,配了 31 个测试用例、6 个 describe 块,commit 35659c4 body 里的验收话就是「31 测试全绿」。

数据迁移最怕的不是崩,是静默错。字段串位、日期偏一天,跑完全绿你也看不出来,只能靠纯函数加单测顶着。

时区那个坑值得单独说。js-yaml 的默认 schema 会把 date: 2024-07-31 03:56:40 这种无时区串直接解析成 Date,按 UTC 算,时区当场就错了。

解法是换 JSON_SCHEMA 让它保持字符串,再自己按 +08:00 解析。scripts/lib/parse.ts:12-13 的注释原文:

js-yaml 是 gray-matter 的依赖,这里直接复用。
用 JSON_SCHEMA 让 date: 2024-07-31 03:56:40 保持字符串(默认 schema 会按 UTC 解析成 Date,时区就错了)

数据量也要可核对。迁移完 commit 35659c4 的 body 里记了 work=145 / tag=247 / download_link=2307 / page=22 / dl_entry=89

deploy/README.md:35 挑了前三个写进导入说明,第 48 行另给了条验证 SQL:导入后 select count(*) from work 应为 145。有这一行,换台机器重跑一遍心里才有底哦。

最后一条容易忽略:迁移脚本和前端共用的编解码函数,要抽成零依赖模块。

vns-next 的 src/lib/download-label.ts:1-3 文件头写着:

/**
 * DownloadLink.label 的编码/解码 —— 纯函数、零依赖
 *(迁移脚本与前端共用;不要在这里引入任何 node/解析库依赖)
 */

起因是 commit 404a769 的「splitLabel 抽出零依赖模块防 gray-matter 进客户端包」。

共用函数一旦顺手引了个解析库,它就会顺着 import 链一路爬进客户端 bundle,谁也拦不住。

不用 ORM 的时候

另一个项目干脆没上 ORM,直接用一个 serverless Postgres 的 HTTP driver 写裸 SQL。

省下来的复杂度,会原样还给你的迁移脚本:

  • 那个 driver 一次只能执行一条语句,所以迁移脚本得自己写一个按分号切分的状态机,还要处理 dollar-quoted 块和字符串里的分号
  • 批量写入必须合成单条 unnest upsert。HTTP 驱动下每条语句都是一次独立的网络往返,条数一多耗时就很难看了
  • 迁移用纯文件名序号、每次全量重跑,靠 SQL 自身幂等,连迁移状态表都没有

最后这条反而最省心。因为没有状态表,也就没有「状态表和真实 schema 不一致」这种最难查的问题喽。

我现在的几条习惯

做法解决什么使用频率
schema 先定死再让 AI 写查询避免它按记忆猜字段★★★★★
迁移脚本必须幂等可重跑中途失败能直接再跑一遍★★★★★
解析/编解码抽成纯函数配单测数据迁移最怕静默错★★★★
原生索引单独一份 SQLpush/generate 都不会帮你建★★★
构建期喂假连接串Docker 构建层里没有数据库★★★

先写到这,等下次换 ORM 再补喔~