很多项目从原型期的 SQLite 起步,业务增长后必须迁移到 MySQL/PostgreSQL。Prisma 让这次迁移比传统 ORM 平滑得多,但仍有不少"文档没细说"的坑。本文基于两次真实迁移复盘,给出完整操作手册。
一、迁移决策:什么时候该离开 SQLite
| 信号 | 说明 |
|---|---|
| 并发写超过个位数 QPS | SQLite 写操作是整库锁,多实例部署直接不可行 |
| 需要多台应用服务器 | 文件型数据库无法跨机共享 |
| 数据量接近百万行 + 复杂查询 | 无服务端连接池与查询缓存,复杂 JOIN 明显变慢 |
| 运维要求主从备份、定时快照 | 需要真正的数据库服务 |
单机展示型官网、访问量日 UV 千级以内的后台系统,SQLite 依然是最省心的选择——不要为不存在的规模提前付费。
二、第一步:改 datasource 与类型映射
datasource db {
provider = "mysql" // sqlite → mysql
url = env("DATABASE_URL")
}
model News {
id String @id @default(cuid())
title String @db.VarChar(255) // MySQL 需要显式长度
content String @db.LongText // Text → LongText 视内容而定
price Decimal? @db.Decimal(10, 2) // 金额务必 Decimal 而非 Float
createdAt DateTime @default(now())
@@index([status, publishedAt])
}
高频类型坑:
- String 无限长:SQLite 随便存,MySQL 的
VARCHAR必须定长;超长文本用@db.Text/@db.LongText - Boolean:MySQL 底层是
TINYINT(1),行为一致无需处理 - DateTime 时区:MySQL
DATETIME不存时区。Prisma 默认 UTC 读写,连接串加?connection_limit=10&pool_timeout=20,展示层统一转 +8,切勿存"本地时间字符串" - Decimal 代替 Float:金额、单价迁移时顺手改成
Decimal,Float 精度事故在订单场景必炸
三、第二步:数据搬迁脚本(停机窗口最小化)
推荐"导出 JSON → 导入"两段式,简单可控:
// export.js —— 从旧库导出
const { PrismaClient } = require('@prisma/client');
const fs = require('fs');
const prisma = new PrismaClient({ datasources: { db: OLD_URL } });
(async () => {
const news = await prisma.news.findMany({ include: { tags: true } });
fs.writeFileSync('dump/news.json', JSON.stringify(news));
console.log('exported', news.length);
await prisma.$disconnect();
})();
// import.js —— 写入新库(分批 + 事务)
const prisma = new PrismaClient({ datasources: { db: NEW_URL } });
const items = JSON.parse(fs.readFileSync('dump/news.json', 'utf-8'));
for (let i = 0; i < items.length; i += 200) {
const batch = items.slice(i, i + 200);
await prisma.$transaction(
batch.map((n) => prisma.news.create({
data: {
id: n.id, title: n.title, content: n.content,
createdAt: new Date(n.createdAt), // 显式还原时间,别让 default(now()) 重写
updatedAt: new Date(n.updatedAt),
},
})),
);
console.log('imported', Math.min(i + 200, items.length));
}
两个细节决定成败:
- 保留原 id 与时间戳:外键关系、SEO 已收录 URL 都依赖 id/slug,新建让
default()重写就全断了 - 导入顺序:先导无外键的主表,再导关联表;或临时关闭外键检查(MySQL
SET FOREIGN_KEY_CHECKS=0),导完恢复并全量校验
四、第三步:schema 管理从 push 切换到 migrate
SQLite 原型期大家都用 prisma db push(无迁移历史)。上生产必须切 migrate,否则无法审计与回滚:
npx prisma migrate dev --name init_mysql # 生成迁移文件 + 应用
npx prisma migrate deploy # CI/生产环境只执行这一条
注意:migrate dev 会在本地生成 prisma/migrations/ 目录,必须提交进 git,它就是你的数据库变更历史。
五、迁移后必做的性能检查
- 补索引:SQLite 的查询计划与 MySQL 不同,用
EXPLAIN复查核心查询(列表页过滤字段、外键、排序字段) - 连接池参数:连接串
connection_limit按 CPU × 2~4 设置;Serverless 场景用 Prisma Accelerate 或 PgBouncer 类方案,避免连接数打爆 - 慢查询日志:开启
slow_query_log阈值 1 秒,观察一周再收口 - 备份策略:每日全量 + binlog 增量,并真实演练一次恢复——没恢复过的备份等于没有备份
六、回滚预案
迁移窗口内保留旧库只读运行,新库出问题 5 分钟内把连接串切回去。DNS/环境变量层面的切换开关比"数据再搬回来"便宜一万倍。
小结
数据库迁移的风险集中在类型映射与历史数据保真两处:类型对了,新库才不炸;id/时间保真了,SEO 与关联才不断。Prisma 把大部分脏活抽象掉了,剩下的正是需要人工把关的部分。