Epic Stack 架构决策实录:移除 cleanupDb 工具,用文件复制与 prisma migrate reset 重构数据库重置流程
【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack
本篇技术文章围绕 Epic Stack 仓库中的架构决策文档 docs/decisions/038-remove-cleanup-db.md 展开,深入剖析项目为何移除cleanupDb数据库清理工具,以及如何用"复制 base.db 文件"和"prisma migrate reset"两条更简单的路径分别取代测试与 seed 场景中的数据库重置。读完本文,你将理解 Prisma 迁移表为何是"实现细节"、文件级数据库复制为什么能做到纳秒级重置,以及 seed 脚本依赖空数据库时的正确命令序列。
决策背景:cleanupDb 存在的原因与痛点
它原本解决什么问题
在 Epic Stack 早期的测试架构中,存在一个名为cleanupDb的工具函数:它会删除数据库中除 Prisma 迁移表之外的所有表,从而让每个测试用例都能从一个干净的数据集开始。
这个工具的存在有明确动机:低层测试(如服务层、工具函数测试)需要频繁重置数据库状态,而当时的标准方案prisma migrate reset需要完整执行"回滚 → 重建表 → 重新应用迁移 → 运行 seed"的流程,对测试场景来说太慢了,无法在每个测试之间反复执行。
同时,seed 脚本也依赖它:在 prisma/seed.ts 正式向数据库写入数据之前,先用cleanupDb清空旧数据,确保 seed 在一个"空库"前提下进行——从源码可以看到,seed 脚本使用prisma.user.create、prisma.note.create直接创建记录并硬编码了id(如d27a197e),这决定了它必须从一个空库开始,否则会因主键冲突或数据残留而失败。
为什么它是"坏味道"
决策文档指出,cleanupDb内部引用"Prisma 迁移表"来保留迁移记录、删除其余表。问题恰恰出在这里:迁移表(_prisma_migrations)属于 Prisma 的实现细节,业务代码和测试代码本不该关心它的存在。让一个测试工具去"感知"迁移表的结构,等于把 ORM 的内部机制泄漏到了测试基础设施中:
- 一旦 Prisma 改变迁移表的命名或结构,
cleanupDb就会悄悄失效; - 测试代码被迫"理解"迁移机制,增加了不必要的认知负担;
- 重置逻辑是命令式"删除表"操作,既不幂等也不够声明式,错误地删错表会直接破坏开发环境。
这正是项目在大量测试重构后决定"摆脱这个实现细节"的核心理由。
决策内容:两条路径替换一个工具
决策文档给出了最终方案,可用一张表概括:
| 场景 | 旧方案 | 新方案 | 关键差异 |
|---|---|---|---|
| 测试间重置数据库 | 调用cleanupDb删表 | 复制base.db到test.db | 纳秒级文件复制,无 Prisma 参与 |
| seed 前清空数据库 | seed 脚本内调用cleanupDb | 直接运行prisma migrate reset | reset 自带重建 + 运行 seed,一步到位 |
需要强调的是,决策文档删除了cleanupDb工具本身(当前仓库中已不存在该工具的任何源码),并同步更新 CI:以prisma migrate reset替代原先的prisma db seed。
方案一:测试用 base.db 文件复制重置数据库
工作原理
决策文档揭示了一个关键洞察:项目早就在所有测试运行之前复制一份"全新数据库"(即base.db)作为起点,既然如此,为什么不把同一思路用到"每次测试之间"?答案是:直接把全新的base.db复制为当前测试进程的test.db。
从源码可以验证这套机制的完整实现:
- tests/setup/global-setup.ts 定义并生成基准库:
BASE_DATABASE_PATH指向./tests/prisma/base.db。setup()会先检查该文件是否存在,并对比其修改时间与prisma/schema.prisma的修改时间——若 schema 更新了,就重新执行npx prisma migrate reset --force --skip-seed --skip-generate(指向file:${BASE_DATABASE_PATH})来重建基准库; - tests/setup/db-setup.ts 在
beforeEach中执行核心逻辑:
beforeEach(async () => { await fsExtra.copyFile(BASE_DATABASE_PATH, databasePath) })同时,db-setup.ts在模块加载时根据VITEST_POOL_ID为每个测试进程分配独立的数据库文件(./tests/prisma/data.${poolId}.db),实现测试并行隔离;afterAll中则动态 import Prisma 客户端并清理临时数据库文件。
为什么"纳秒级"
决策文档明确说复制文件"takes nanoseconds"。底层原理是:SQLite 本身就是单文件数据库,而base.db是经过完整迁移的全新空库(含全部表结构、RBAC 角色种子数据,但不含业务数据)。文件复制不涉及 SQL 执行、索引重建、迁移校验,只是一次磁盘拷贝,因此比任何"连接数据库执行 DROP/CREATE"的方式都快几个数量级。这一方案能成立的前提,正是本项目选用 SQLite 单文件存储(见 docs/database.md 与决策文档 003-sqlite.md)。
方案二:seed 前用 prisma migrate reset 清空并重建
seed 脚本为何要求空库
如前所述,prisma/seed.ts 中的 seed 逻辑以"数据库为空"为前提:它通过prisma.user.create创建 5 个随机用户并为其生成笔记与图片,再创建管理员kody,连接 GitHub OAuth 账号,最后硬编码id写入 12 篇 kody 笔记。若数据库里已有数据,重复执行会抛唯一约束错误。
migrate reset 如何一步到位
Prisma 的prisma migrate reset会完整执行:删除数据库 → 重新创建 → 应用全部迁移 → 执行 seed 脚本(seed 命令在 package.json 的"prisma": { "seed": "tsx prisma/seed.ts" }中声明)。因此它天然满足"空库 + seed"的组合需求,完全替代了旧方案中"先cleanupDb清空、再 seed"的两步操作:
npx prisma migrate reset在 CI 中,.github/workflows/deploy.yml 的 Playwright 任务正是这样使用的——先用actions/cache@v4缓存数据库,缓存未命中时执行npx prisma migrate reset --force完成建库与 seed:
- name: 🌱 Seed Database if: steps.db-cache.outputs.cache-hit != 'true' run: npx prisma migrate reset --force保留迁移表的正确姿势
prisma migrate reset会自动重建 Prisma 的迁移记录表(_prisma_migrations)并重新应用 prisma/migrations 下全部迁移(当前仓库包含20250221233640_init初始迁移)。这正是决策文档强调的对比点:旧cleanupDb需要手写逻辑"保留迁移表、删除其余表",把实现细节暴露给调用方;而migrate reset由 Prisma 官方管理迁移状态,调用方完全不需要关心迁移表是否存在、叫什么名字。
决策后果:prisma db seed 的直接行为变化
决策文档明确记载了最重要的一个后果:移除cleanupDb之后,直接在非空数据库上运行npx prisma db seed将会失败,因为 seed 脚本期待一个空数据库。
对此项目给出了两条出路:
- 推荐做法:改用
npx prisma migrate reset来 seed 数据库——它先重置再 seed,效果等同于旧方案(先清理再 seed); - 备选做法:改造 seed 脚本,用 upsert 代替 create 使其幂等,但这会显著增加脚本复杂度,项目明确不倾向此路线。
需要说明的是,这一后果主要影响"重复执行db seed"的本地开发场景;对首次搭建环境而言,docs/getting-started.md 中给出的初始化流程(npm run setup执行prisma migrate deploy)与 seed 命令(npx prisma@6 db seed)在空库上仍可正常工作。而生产环境的迁移与初始化另有机制:部署时由主实例执行npx prisma migrate deploy(见 other/litefs.yml),生产 seed 的推荐做法是直接修改migration.sql以保证可复现(详见 docs/database.md 的"Seeding Production"章节)。
迁移经验总结:这条决策能给其他项目什么启发
从 Epic Stack 移除cleanupDb的决策中,可以提炼出几条可迁移到任何 Prisma + SQLite 项目的经验:
- 测试重置数据库的首选是"文件快照复制"而非"SQL 清理":当数据库是单文件(SQLite)时,复制一份经过完整迁移的空库,比任何删表逻辑都快且无副作用,前提是保证测试进程的文件隔离(本项目用
VITEST_POOL_ID实现); - 不要让业务/测试代码感知 ORM 的内部表:凡是要"保留某张系统表"的清理逻辑都是坏味道,应交给官方工具管理迁移状态;
- seed 与 reset 强绑定:若 seed 脚本依赖空库,就应通过
prisma migrate reset(重置 + seed 一步完成)作为唯一入口,而不是在 seed 内部做清理; - CI 中善用缓存与 reset 组合:如 .github/workflows/deploy.yml 所示,用 schema/migration 的哈希作为缓存键,仅在缓存未命中时执行
migrate reset,既保证环境一致性又节省 CI 时间。
本决策的完整上下文记录在 docs/decisions/README.md,相关工具链的进一步使用方式可参考 docs/database.md(数据库与迁移)与 docs/skills/epic-database/SKILL.md(数据库技能指南)。
【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考