Node.js MySQL 数据库
前面几篇的 API 数据都存在内存数组里,进程一重启就全没了。这篇解决怎么把数据持久化到 MySQL,包括环境准备、连接池、参数化查询和完整的增删改查。数据库密码这类敏感信息要放进 .env 文件,做法参考 Node.js 环境变量与配置。
环境准备
方式一,本地安装 MySQL
从官网下载安装包,一路下一步即可。macOS 用 brew install mysql,Ubuntu 用 sudo apt install mysql-server。安装完启动服务,再设置 root 密码。
方式二,Docker 一条命令
电脑装了 Docker 的话,一条命令就能启动 MySQL 8 容器
docker run --name mysql8 \
-e MYSQL_ROOT_PASSWORD=123456 \
-e MYSQL_DATABASE=blog \
-p 3306:3306 \
-d mysql:8
MYSQL_ROOT_PASSWORD 设置 root 密码,MYSQL_DATABASE 会在容器首次启动时自动建好 blog 库,-p 3306:3306 把容器的 3306 端口映射到本机。
连接信息
| 项目 | 值 |
|---|---|
| host | 127.0.0.1 |
| port | 3306 |
| user | root |
| password | 123456 |
| database | blog |
安装 mysql2
npm install mysql2
mysql2 是社区最流行的 MySQL 驱动,原生支持 Promise,配合 async/await 写起来很顺。老驱动 mysql 还在用回调,新手直接选 mysql2。
创建连接池
连接池的作用
每次查询都新建连接很浪费,连接池一次建好一批连接,用的时候取一条,用完还回去。并发高时连接总数不会超过上限,避免把数据库压垮。
连接串格式
mysql2 支持两种写法,对象和连接串
mysql://user:pass@host:port/db
实际开发推荐用连接串配合环境变量,密码不写死在代码里。
// db.js
const mysql = require("mysql2/promise");
require("dotenv").config();
const pool = mysql.createPool(process.env.DATABASE_URL, {
connectionLimit: 10
});
module.exports = pool;
.env 文件里这样写
DATABASE_URL=mysql://root:123456@127.0.0.1:3306/blog
连接用完不需要手动释放,池会自动把连接还回去,忘关闭也不会泄漏连接。
参数化查询
用 ? 占位符
mysql2 的 execute 方法支持 ? 占位符,参数单独传数组
const [rows] = await pool.execute(
"SELECT * FROM users WHERE email = ?",
[email]
);
拼接字符串的灾难
// 危险写法,绝对不要这样拼 SQL
const sql = `SELECT * FROM users WHERE email = '${email}'`;
如果用户输入 x' OR '1'='1,拼出来的 SQL 变成 WHERE email = 'x' OR '1'='1',条件恒为真,整张表的数据全被查走。参数化查询把输入当纯数据处理,数据库根本不会把它当成 SQL 执行。这是安全底线,Node.js Web 安全 还会细讲。
增删改查
先建一张用户表
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
插入,返回 insertId
const [result] = await pool.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
["小明", "xiaoming@example.com"]
);
console.log(result.insertId); // 新记录的自增 id
查询,返回数组
const [rows] = await pool.execute("SELECT * FROM users");
console.log(rows); // 数组,空表时是空数组
const [row] = await pool.execute(
"SELECT * FROM users WHERE id = ?",
[1]
);
console.log(row[0]); // 单条记录,查不到是 undefined
更新,看影响行数
const [result] = await pool.execute(
"UPDATE users SET name = ? WHERE id = ?",
["小红", 1]
);
console.log(result.affectedRows); // 改了 1 行
删除
const [result] = await pool.execute(
"DELETE FROM users WHERE id = ?",
[1]
);
console.log(result.affectedRows); // 删了 1 行
注意 execute 返回的数组第一个元素才是结果,第二个是字段元数据,新手经常忘记解构。
错误处理
连接失败先看错误码
| 报错 | 原因 |
|---|---|
| ECONNREFUSED | MySQL 服务没启动,或端口不对 |
| ER_ACCESS_DENIED_ERROR | 用户名或密码错误 |
| ER_BAD_DB_ERROR | 数据库名不存在 |
| ER_NO_SUCH_TABLE | 表还没建 |
try {
const [rows] = await pool.execute("SELECT 1");
console.log("连接成功");
} catch (err) {
console.error("连接失败", err.code);
}
实践
写一个用户管理模块,把 Node.js RESTful API 设计 里的内存数组换成 MySQL。
// userModel.js
const pool = require("./db");
async function list() {
const [rows] = await pool.execute("SELECT * FROM users");
return rows;
}
async function getById(id) {
const [rows] = await pool.execute("SELECT * FROM users WHERE id = ?", [id]);
return rows[0];
}
async function create(name, email) {
const [result] = await pool.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
[name, email]
);
return getById(result.insertId);
}
async function update(id, name) {
await pool.execute("UPDATE users SET name = ? WHERE id = ?", [name, id]);
return getById(id);
}
async function remove(id) {
await pool.execute("DELETE FROM users WHERE id = ?", [id]);
}
module.exports = { list, getById, create, update, remove };
// test.js
const userModel = require("./userModel");
(async () => {
const created = await userModel.create("小明", "xm@example.com");
console.log("新增", created);
const all = await userModel.list();
console.log("列表", all);
await userModel.update(created.id, "小明改");
console.log("改后", await userModel.getById(created.id));
await userModel.remove(created.id);
console.log("删后数量", (await userModel.list()).length);
})();
常见坑
- 忘记解构
execute的返回,拿到的是[rows, fields]数组。 - 手写字符串拼接 SQL,被注入直接整库沦陷,一律用
?占位符。 - 数据库密码写死在代码里,要放进 .env 并用 dotenv 加载。
- 端口写错或服务没启动,报 ECONNREFUSED 第一反应去查这两项。
- 用完连接去调
pool.end(),池会自动管理,不需要手动关闭。
手写 SQL 容易出错,下一篇换成 ORM 自动生成这些语句,见 Node.js Prisma ORM。