# 项目建表初始化
30 秒速记
- 做法:
SQL脚本放sql/目录 →Node读出来 → 按;拆成单条 → 逐条await query执行 - 建表用
CREATE TABLE IF NOT EXISTS,重复跑不报错;但改表结构它管不了 - 按
;拆分是简化做法,字符串或存储过程里带分号就会拆错 - 执行完要
pool.end(),不然连接池挂着,进程不退出 - 正式项目用迁移工具:
Knex/Sequelizemigration、Prisma Migrate、Flyway
建表初始化就是把建表 SQL 存成文件放进仓库,写个脚本一键执行,新人拉代码、换环境都能一条命令把库建好。 流程很简单:遍历 sql/ 目录拿到所有 .sql 文件,读出内容,按分号拆成单条语句,依次执行并打印成败。它适合学习和小项目,但有个明显的局限:IF NOT EXISTS 只管「没有就建」,表已经存在又要加字段,它就无能为力了。真实项目表结构会不断演进,我会用迁移工具,每次变更一个版本号文件,记录执行到了哪一版。
理解「项目建表初始化」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
项目建表初始化这件事,核心是让「数据库结构」和代码一起进版本管理。下面是一个比较稳的最小实现,可以直接拿来改:
// scripts/init-db.js
const fs = require('fs')
const path = require('path')
const mysql = require('mysql2/promise')
async function main() {
const conn = await mysql.createConnection({
host: '127.0.0.1',
user: 'root',
password: process.env.DB_PASSWORD,
database: 'koa_demo',
multipleStatements: true, // 允许一次执行整个文件里的多条语句
})
const dir = path.join(__dirname, '../sql')
const files = fs.readdirSync(dir).filter(f => f.endsWith('.sql')).sort() // 001_xxx.sql 保证顺序
try {
for (const file of files) {
const sql = fs.readFileSync(path.join(dir, file), 'utf8')
await conn.query(sql)
console.log(`[SUCCESS] ${file}`)
}
} catch (err) {
console.error(`[ERROR] ${err.code}: ${err.message}`)
process.exitCode = 1 // 让 CI 能感知失败
} finally {
await conn.end() // 关掉连接,进程自然退出
}
}
main()
这里有几处和「按分号拆分逐条执行」不同的考虑:
multipleStatements: true让驱动整份文件一起发,不用自己拆,字符串里带分号也不会出错。代价是这个选项会放大注入风险,所以只在初始化脚本里开,业务连接池别开。- 文件按名字排序,用序号前缀控制执行顺序,有外键依赖时很重要。
- 失败时设置
process.exitCode = 1,CI或部署脚本才能发现初始化失败了。
当表结构开始频繁变化,就该换成迁移工具了:每次变更写一个新的迁移文件,工具在库里记录执行到哪一版,只跑新的,必要时还能回滚。
面试官追问
追问 1表已经存在,现在要给 user 表加一个 avatar 字段,再跑这个初始化脚本有用吗?
没用,CREATE TABLE IF NOT EXISTS 发现表存在就直接跳过了。要写 ALTER TABLE user ADD COLUMN avatar VARCHAR(255),而且要记录这条执行过没有,这正是迁移工具做的事。
追问 2脚本跑完打印了「执行结束」,但命令行一直不退出,为什么?
连接池里还有活着的连接,事件循环不会结束。脚本最后加一句 pool.end() 就能自己退出,不用让人按 Ctrl + C。
追问 3user.sql 里有 INSERT 初始数据,跑两次会怎样?
建表那句因为 IF NOT EXISTS 没事,INSERT 会再插一遍,数据重复了。种子数据要用 INSERT IGNORE 配唯一索引,或者和建表脚本分开、只在空库时执行。
追问 4这种脚本和 Prisma Migrate 比,差在哪?
缺版本记录。迁移工具会在库里建一张表记录哪些迁移已执行,每次只跑新的,还能回滚;这个脚本每次全量重跑,只能靠 IF NOT EXISTS 兜底。
追问 5两张表有外键关系,脚本执行顺序乱了会怎样?
先建子表会因为引用的父表不存在而失败。readdirSync 的顺序不保证,文件名加序号前缀比如 001_user.sql,读出来后 sort() 一下再执行。
# 建表初始化
# 前言
30 秒速记
- 痛点:项目开发期表结构经常变,手动在客户端里建表容易漏、各环境不一致
- 解法:建表
SQL放进仓库的sql/目录,写个脚本一键重建 - 本质是「数据库结构即代码」,和代码一起
review、一起回滚 - 局限:只能全量重建,改表结构、保留旧数据要靠迁移工具
为什么要封装建表初始化?因为开发阶段表结构天天在改,靠人在图形化客户端里点很容易漏,把 SQL 放进仓库就能保证每个人、每个环境建出来的表都一样。 新同事拉下代码,执行一条 node index.js 就有了完整的库,不用找人要建表语句。这种「全量重建」的思路在开发阶段很好用,因为数据可以随便丢;一旦上线有了真实数据,就不能删了重建,要换成增量迁移。
通常初始化数据库要建立很多表,特别在项目开发的时候表的格式可能会有些变动,这时候就需要封装对数据库建表初始化的方法,保留项目的sql脚本文件,然后每次需要重新建表,则执行建表初始化程序就行
面试官追问
追问 1开发环境随便重建,那上线之后表结构要改怎么办?
上线后不能删表重建,要写增量的 ALTER 语句,并且记录执行过哪些。这就是迁移工具的工作,每次变更一个文件,按顺序执行,库里有一张表记着当前版本。
追问 2同事在客户端里手动给表加了个索引,没写进 SQL 文件,会有什么后果?
他本地快了,别人和线上都没有这个索引,问题很难复现。所有结构变更都必须走仓库里的脚本,客户端只用来查数据。
追问 3测试环境每次跑 CI 都要一个干净的库,这套脚本能用上吗?
正好合适。CI 里起一个 MySQL 容器,跑一遍初始化脚本建表、插种子数据,再跑测试,每次都是干净且一致的。
追问 4建表 SQL 放仓库里,有没有什么不能放进去的?
真实用户数据、生产账号密码不能放。种子数据只放测试用的假数据,连接配置走环境变量。
# 快速开始
30 秒速记
- 准备:本地
MySQL跑起来、建好koa_demo库,db.js里的账号密码改成自己的 - 运行:
npm install→node index.js,控制台逐条打印SQL执行结果 - 验证:客户端里看
user、data两张表建好了,user里有3条初始数据 - 脚本不会自动退出,要手动
Ctrl + C;加一句pool.end()就能自己结束
跑这个 demo 前要先把数据库准备好,脚本只负责建表,不负责建库。 也就是说 koa_demo 这个库得先 CREATE DATABASE 出来,db.js 里的 host、user、password 改成你本地的,不然一上来就是连接报错。跑完之后别只看控制台打印的成功,打开客户端确认表结构和数据确实写进去了。如果用的是 MySQL 8 加老的 mysql 包,大概率会遇到认证报错,换 mysql2 最省事。
面试官追问
追问 1一跑就报 ER_BAD_DB_ERROR: Unknown database 'koa_demo',怎么回事?
库还没建。脚本连接时指定了 database,库不存在就直接失败。先 CREATE DATABASE koa_demo DEFAULT CHARSET utf8mb4 再跑。
追问 2控制台全是 SUCCESS,怎么确认数据真的写进去了?
到客户端里执行 SHOW TABLES 和 SELECT COUNT(*) FROM user,看到两张表和 3 条数据才算真的成功。日志只是程序自己说的。
追问 3想让脚本跑完自己退出,怎么改?
所有 SQL 执行完后调用 pool.end(),连接都关掉了,事件循环没事可做,进程就会退出。
