六维教程

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

上一篇
Node.js 环境变量与配置
下一篇
Node.js Prisma ORM