返回博客
工程 · ·8 min

重写社区平台栈,但不重写数据库

lakebbs 从 Vue 2 + Express + Sequelize 迁到 Next.js 16 + React 19 + Drizzle ORM —— 但 MySQL 5.7 schema 没动。为什么栈重写时锁住数据库是无聊但正确的决定。

  • #nextjs
  • #drizzle
  • #mysql
  • #migration
  • #engineering-discipline
  • #rewrite

lakebbs 是北安省小城市的中文社区平台。2024 年起步用的是当时的实用栈:Vue 2 + Vite + Pinia + Express + Sequelize + MySQL 5.7。两年后这套栈变成累赘 —— 类型松散、没有 SSR、AI 重构工具在 Vue 2 上远弱于现代 TypeScript / React。

2026 年 5 月落地的重写把整个 app 搬到了 Next.js 16 (App Router) + React 19 + TypeScript + Drizzle ORM + NextAuth v5 + Tailwind 4。有意思的地方:MySQL schema 没动。没加列、没删列、没改类型。同一个数据库、同一份数据、同一个 nginx 反代后的 lakebbs MySQL 5.7 实例。

重写时如果连 schema 也改,你会同时面对:

  1. 代码项目 —— 写新组件、新 ORM 模型、新路由
  2. 数据项目 —— 写迁移脚本变换已有行
  3. 部署项目 —— 协调代码切换和数据切换,两边都要 rollback 路径
  4. 正确性项目 —— 验证新 app 在新 schema 下产出跟旧 app 在旧 schema 下相同

每个项目都有自己的失败模式。合起来的失败模式不是相加,是相乘。

锁住 schema 后:(1) 变成纯代码项目;(2) 空;(3) 一次切换 nginx;(4) 检查新 app 跟旧 app 输出一致。总工作量被 (1) 上限。而 (1) 就是我本来想做的事。

实际看 Drizzle 怎么对接现有 schema:

export const posts = mysqlTable('posts', {
  id: int('id').primaryKey().autoincrement(),
  cityId: int('city_id').notNull(),
  authorId: int('user_id').notNull(),  // 保留遗留列名
  title: varchar('title', { length: 255 }).notNull(),
  body: text('body').notNull(),
  isPinned: tinyint('is_pinned').default(0).notNull(),  // Sequelize 遗产
  createdAt: datetime('created_at').notNull(),
  updatedAt: datetime('updated_at').notNull(),
});

数据库的遗留留在数据库,新代码允许干净。

线上切换是一次 nginx reload。旧 Express 停、新 Next.js 起,都指向同一个 MySQL。零用户感知 downtime。重写在几周而不是几个月内完成。Rollback 路径就是「停新 app,重启旧 app」—— 同 schema,同数据。

完整论证 + 哪些场景不适用这个 pattern(迁移数据库引擎、合规需求、schema 本身就是要解决的问题等)见 英文版

相关

— 相关阅读