为什么要在Node.js中建表
把建表动作放在Node.js里执行,不是为了替代数据库管理工具,而是为了解决几个实际场景:项目初始化时需要自动准备表结构、多环境部署时要保证开发库和测试库结构一致、CI流程中需要脚本化地重建测试表。MySQL Workbench适合人工设计和调整,Node.js脚本适合纳入版本控制和自动化流程。两者定位不同,不存在谁替代谁的问题。
本文围绕mysql这个Node.js驱动包展开。需要注意,mysql包目前处于维护状态,mysql2是更活跃的替代方案,API高度兼容且支持Promise。本文以mysql包为主线讲解,在末尾会说明mysql2的惯用写法,便于你根据项目情况选择。
环境准备与项目初始化
开始之前需要确认本机已安装Node.js、MySQLServer,并准备一个代码编辑器。JavaScript和SQL的基础语法需要熟悉,否则后续排查错误会比较吃力。
新建项目目录并初始化:
mkdir node-mysql-demo
cd node-mysql-demo
npm init -y
npminit-y会生成package.json,省去交互式填写。接着安装mysql包:
npm install mysql
安装完成后,package.json的dependencies中会出现mysql条目。这一步的意义在于把驱动依赖固化到项目配置中,换一台机器执行npminstall就能还原环境,而不是靠记忆手动装包。
准备目标数据库
建表之前必须先有数据库。用MySQL命令行或Workbench执行:
CREATE DATABASE employeedb;
USE employeedb;
这里没有用CREATEDATABASEIF NOT EXISTS,是因为数据库通常只创建一次,重复执行报错反而能提醒你当前连接到了哪个实例。建表语句则不同,后面会专门讲IF NOT EXISTS的用法。
建立Node.js与MySQL的连接
在项目根目录新建app.js,写入连接代码:
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'employeedb'
});
connection.connect((err) => {
if (err) {
console.error('Connection failed:', err.message);
return;
}
console.log('Connected to MySQL database!');
});
几个参数的含义需要说清楚。host指向MySQL服务地址,本地开发一般是localhost;user和password是数据库账号凭据,password必须替换成真实密码,直接写明文在生产环境不可取,后面会给出改进方向;database指定默认操作的库,省去每次查询都写库名。
connection.connect()的回调里先判断err,这是Node.js回调风格的基本习惯。出错时打印err.message而不是整个err对象,输出更干净,定位问题也更直接。
建表语句的设计
employees表的结构如下:
CREATE TABLE employees (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
age INT,
email VARCHAR(100)
);
id设为INT主键并加AUTO_INCREMENT,插入数据时不需要手动指定,MySQL会自动递增。name和email用VARCHAR(100),100是字符长度上限,对姓名和常规邮箱足够。age用INT,不需要小数。
这里有个经验点:email字段如果业务上要求唯一,应该加UNIQUE约束,而不是靠应用层查重。数据库层约束才是之后一道防线,应用层判断存在并发漏洞。本文为保持示例简洁未加,实际项目中建议补上。
在Node.js中执行建表
把建表语句放进连接回调中执行:
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'employeedb'
});
connection.connect((err) => {
if (err) {
console.error('Connection failed:', err.message);
return;
}
console.log('Connected to MySQL database!');
const sql = `
CREATE TABLE employees (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
age INT,
email VARCHAR(100)
)
`;
connection.query(sql, (err, result) => {
if (err) {
console.error('Error creating table:', err.message);
connection.end();
return;
}
console.log('employees table created successfully!');
connection.end();
});
});
connection.query()负责发送SQL并接收结果。回调中的err和result分别对应失败和成功两种情况。无论成功失败,都调用了connection.end()关闭连接。这一点容易被忽略:脚本执行完不关连接,Node.js进程可能挂起不退出,在定时任务或批处理脚本中会造成资源浪费。
运行nodeapp.js,正常输出为:
Connected to MySQL database!
employees table created successfully!
验证表是否创建成功
在MySQL客户端执行:
SHOW TABLES;
可以看到employees。再用DESCRIBE查看结构:
DESCRIBE employees;
返回的Field、Type、Key三列分别对应字段名、数据类型和键类型,id行的Key显示为PRI,说明主键生效。养成建表后验证的习惯,比等到插入数据报错再回头查要省事。
重复建表的处理:IF NOT EXISTS
第二次执行同样的CREATETABLE会直接报错,提示表已存在。在自动化脚本中这通常不是想要的结果。改用:
CREATE TABLE IF NOT EXISTS employees (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
age INT,
email VARCHAR(100)
);
Node.js中写法一致:
const sql = `
CREATE TABLE IF NOT EXISTS employees (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
age INT,
email VARCHAR(100)
)
`;
connection.query(sql, (err, result) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Table is ready!');
});
IF NOT EXISTS的语义是:表不存在就建,存在就跳过,不报错。适合初始化脚本反复执行的场景。但要注意它只判断表名,不判断结构。如果表已存在但字段和你的预期不一致,它不会做任何修改,也不会提示。结构变更仍需ALTERTABLE或迁移工具处理。
常见错误与排查方向
| 错误信息 | 可能原因 |
|---|---|
| Accessdeniedforuser | 用户名或密码错误 |
| Unknowndatabase | 指定的数据库不存在 |
| Tablealreadyexists | 同名表已存在,未加IF NOT EXISTS |
| ECONNREFUSED | MySQL服务未启动或地址不可达 |
| SQLsyntaxerror | CREATETABLE语句语法有误 |
err.message是排查的第一手信息,先看它再决定下一步。凭据问题去核对配置,连接问题去检查服务状态,语法问题去比对SQL写法。
项目中的几点反思
第一,密码不要硬编码。开发阶段图方便可以写在代码里,但只要代码进版本库,凭据就泄露了。用环境变量读取是成本较低的改进:
require('dotenv').config();
const connection = mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
});
第二,连接用完要关闭。单次脚本用connection.end(),Web服务中则不应该每次请求都建连接,而要用连接池mysql.createPool()。连接池复用连接,避免频繁握手开销,这是生产环境的常规做法。
第三,建表逻辑和业务逻辑分开。把建表语句单独放在migration目录或初始化脚本中,不要混在业务代码里。表结构变更应该有迹可循,而不是散落在各处。
mysql2的惯用写法
如果项目使用mysql2,建表代码可以写成Promise风格:
const mysql = require('mysql2/promise');
async function createTable() {
const connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: process.env.DB_PASSWORD,
database: 'employeedb'
});
try {
await connection.query(`
CREATE TABLE IF NOT EXISTS employees (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
age INT,
email VARCHAR(100)
)
`);
console.log('Table is ready!');
} catch (err) {
console.error('Error:', err.message);
} finally {
await connection.end();
}
}
createTable();
async/await让错误处理用try/catch/finally表达,比嵌套回调更清晰。mysql2同时支持回调和Promise,迁移成本低,新项目可以优先考虑。
本节课程知识要点
-
mysql包通过createConnection建立连接,query方法执行SQL
-
CREATETABLE定义字段名、数据类型和约束,PRIMARYKEY与AUTO_INCREMENT常配合使用
-
IF NOT EXISTS避免重复建表报错,但不负责结构变更
-
连接使用后需end()关闭,Web场景应改用连接池
-
凭据通过环境变量管理,不硬编码进代码库
-
mysql2提供Promise接口,写法更贴近现在Node.js
练习与思考
-
在employees表的email字段上添加UNIQUE约束,写出完整的CREATETABLE语句。
-
用mysql2改写本文的建表脚本,改为async/await风格。
-
思考:如果表已存在但需要新增一个department字段,IF NOT EXISTS能否满足需求?应该用什么语句?
-
尝试用连接池替换单连接,观察代码结构需要做哪些调整。
Node.js建表本身不复杂,难点在于把连接管理、错误处理、凭据安全和结构变更这几件事放到项目整体中考虑。示例代码能跑通只是起点,真正决定代码质量的是对边界情况的处理。建议从本文示例出发,逐步替换成mysql2和环境变量配置,把建表脚本纳入项目的初始化流程,而不是每次手动执行。