很多项目从原型期的 SQLite 起步,业务增长后必须迁移到 MySQL/PostgreSQL。Prisma 让这次迁移比传统 ORM 平滑得多,但仍有不少"文档没细说"的坑。本文基于两次真实迁移复盘,给出完整操作手册。

一、迁移决策:什么时候该离开 SQLite

信号说明
并发写超过个位数 QPSSQLite 写操作是整库锁,多实例部署直接不可行
需要多台应用服务器文件型数据库无法跨机共享
数据量接近百万行 + 复杂查询无服务端连接池与查询缓存,复杂 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));
}

两个细节决定成败:

  1. 保留原 id 与时间戳:外键关系、SEO 已收录 URL 都依赖 id/slug,新建让 default() 重写就全断了
  2. 导入顺序:先导无外键的主表,再导关联表;或临时关闭外键检查(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 把大部分脏活抽象掉了,剩下的正是需要人工把关的部分。