Node系列 · ORM:mysql 驱动程序
Node 访问 MySQL 必须通过"驱动"------它把 JS 的
query()调用翻译成 MySQL 协议包发给数据库,再把响应解析成 JS 对象。mysql和mysql2是两个最主流的驱动,mysql2维护更活跃、性能更好,是新项目的首选。
一、什么是驱动程序
驱动程序(Driver)是应用与数据库之间的翻译层:
#mermaid-svg-G5LsgeTQDhHgxE0z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-G5LsgeTQDhHgxE0z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-G5LsgeTQDhHgxE0z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-G5LsgeTQDhHgxE0z .error-icon{fill:#552222;}#mermaid-svg-G5LsgeTQDhHgxE0z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-G5LsgeTQDhHgxE0z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-G5LsgeTQDhHgxE0z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-G5LsgeTQDhHgxE0z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-G5LsgeTQDhHgxE0z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-G5LsgeTQDhHgxE0z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-G5LsgeTQDhHgxE0z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-G5LsgeTQDhHgxE0z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-G5LsgeTQDhHgxE0z .marker.cross{stroke:#333333;}#mermaid-svg-G5LsgeTQDhHgxE0z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-G5LsgeTQDhHgxE0z p{margin:0;}#mermaid-svg-G5LsgeTQDhHgxE0z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-G5LsgeTQDhHgxE0z .cluster-label text{fill:#333;}#mermaid-svg-G5LsgeTQDhHgxE0z .cluster-label span{color:#333;}#mermaid-svg-G5LsgeTQDhHgxE0z .cluster-label span p{background-color:transparent;}#mermaid-svg-G5LsgeTQDhHgxE0z .label text,#mermaid-svg-G5LsgeTQDhHgxE0z span{fill:#333;color:#333;}#mermaid-svg-G5LsgeTQDhHgxE0z .node rect,#mermaid-svg-G5LsgeTQDhHgxE0z .node circle,#mermaid-svg-G5LsgeTQDhHgxE0z .node ellipse,#mermaid-svg-G5LsgeTQDhHgxE0z .node polygon,#mermaid-svg-G5LsgeTQDhHgxE0z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-G5LsgeTQDhHgxE0z .rough-node .label text,#mermaid-svg-G5LsgeTQDhHgxE0z .node .label text,#mermaid-svg-G5LsgeTQDhHgxE0z .image-shape .label,#mermaid-svg-G5LsgeTQDhHgxE0z .icon-shape .label{text-anchor:middle;}#mermaid-svg-G5LsgeTQDhHgxE0z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-G5LsgeTQDhHgxE0z .rough-node .label,#mermaid-svg-G5LsgeTQDhHgxE0z .node .label,#mermaid-svg-G5LsgeTQDhHgxE0z .image-shape .label,#mermaid-svg-G5LsgeTQDhHgxE0z .icon-shape .label{text-align:center;}#mermaid-svg-G5LsgeTQDhHgxE0z .node.clickable{cursor:pointer;}#mermaid-svg-G5LsgeTQDhHgxE0z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-G5LsgeTQDhHgxE0z .arrowheadPath{fill:#333333;}#mermaid-svg-G5LsgeTQDhHgxE0z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-G5LsgeTQDhHgxE0z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-G5LsgeTQDhHgxE0z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G5LsgeTQDhHgxE0z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-G5LsgeTQDhHgxE0z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G5LsgeTQDhHgxE0z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-G5LsgeTQDhHgxE0z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-G5LsgeTQDhHgxE0z .cluster text{fill:#333;}#mermaid-svg-G5LsgeTQDhHgxE0z .cluster span{color:#333;}#mermaid-svg-G5LsgeTQDhHgxE0z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-G5LsgeTQDhHgxE0z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-G5LsgeTQDhHgxE0z rect.text{fill:none;stroke-width:0;}#mermaid-svg-G5LsgeTQDhHgxE0z .icon-shape,#mermaid-svg-G5LsgeTQDhHgxE0z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G5LsgeTQDhHgxE0z .icon-shape p,#mermaid-svg-G5LsgeTQDhHgxE0z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-G5LsgeTQDhHgxE0z .icon-shape .label rect,#mermaid-svg-G5LsgeTQDhHgxE0z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G5LsgeTQDhHgxE0z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-G5LsgeTQDhHgxE0z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-G5LsgeTQDhHgxE0z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} query SQL
MySQL 协议包
结果集
JS 对象
Node 应用代码
mysql2 驱动
MySQL Server
Node 调用 connection.query('SELECT ...'),驱动做三件事:
- 把 SQL 字符串打包成 MySQL 协议字节流
- 通过 TCP socket 发送给 MySQL Server
- 把返回的字节流解析成 JS 数组 / 对象
二、mysql vs mysql2
| 维度 | mysql |
mysql2 |
|---|---|---|
| 维护状态 | 维护较少 | 持续活跃 |
| 性能 | 一般 | 更快(原生绑定 + 流优化) |
| Promise 支持 | 需要 mysql2/promise 包装 |
内置 mysql2/promise |
| 预处理语句 | 支持 | 支持且更标准 |
| 认证 | 较少 | 支持 caching_sha2_password(MySQL 8 默认) |
| 推荐 | 老项目 | 新项目首选 |
::: tip
MySQL 8.0 默认认证插件是 caching_sha2_password ,老版本 mysql 驱动不支持,会报"Client does not support authentication protocol"。用 mysql2 解决。
:::
三、安装与连接
bash
npm install mysql2
3.1 创建连接池
javascript:mysql-pool.js
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: '127.0.0.1',
port: 3306,
user: 'root',
password: 'your-password',
database: 'myapp',
waitForConnections: true, // 池满时等待(true)还是报错(false)
connectionLimit: 10, // 最大连接数
queueLimit: 0, // 等待队列上限,0 = 不限
});
// 使用
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [1]);
console.log(rows);
3.2 连接池 vs 单连接
| 维度 | 连接池 | 单连接 |
|---|---|---|
| 并发请求 | 多连接并行 | 串行排队 |
| 性能 | 高(建连接成本被复用) | 低(每次要建/拆 TCP) |
| 复杂度 | 需要配置 limit | 简单 |
| 适用 | Web 服务等高并发 | 一次性脚本 |
::: tip
生产环境永远用连接池 。每次 pool.query() 自动从池里取连接、用完归还;连接复用减少 TCP 握手开销。
:::
四、查询 API
4.1 query vs execute
| 方法 | 是否预处理 | 防 SQL 注入 | 性能 | 何时用 |
|---|---|---|---|---|
query(sql, params) |
可选(参数化时是) | ✅ | 一般 | 简单查询 |
execute(sql, params) |
✅ 强制预处理 | ✅ 更安全 | 更快(MySQL 预编译) | 高频重复 SQL |
javascript:mysql-execute.js
// 推荐:execute + 占位符
const [rows] = await pool.execute(
'SELECT * FROM users WHERE email = ? AND status = ?',
['alice@example.com', 'active']
);
4.2 查询结果格式
javascript:mysql-result.js
const [rows, fields] = await pool.execute('SELECT id, name FROM users LIMIT 3');
// rows:行数据数组
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ...]
// fields:列元信息(一般用不到)
// [{ name: 'id', type: 3, ... }, { name: 'name', type: 253, ... }]
五、预处理语句防 SQL 注入
永远用占位符,不要拼接字符串:
javascript:mysql-injection.js
// ❌ 危险:字符串拼接,SQL 注入风险
const userId = req.query.id; // 攻击者传 "1 OR 1=1"
const sql = `SELECT * FROM users WHERE id = ${userId}`;
await pool.query(sql);
// ✅ 安全:占位符
const sql = 'SELECT * FROM users WHERE id = ?';
await pool.query(sql, [userId]);
::: danger
任何用户输入都不能拼进 SQL 。mysql2 的占位符实现是预处理语句------值在协议层和 SQL 分离,攻击者无法通过输入改变 SQL 结构。
:::
六、连接池参数调优
| 参数 | 默认值 | 调优建议 |
|---|---|---|
connectionLimit |
10 | 按 MySQL max_connections 和应用并发量决定;一般 10-30 |
waitForConnections |
true | 高并发设 true(排队等待) |
queueLimit |
0 | 0 = 不限;生产建议设一个上限(如 100),避免请求堆积 |
idleTimeout |
60000ms | 空闲连接回收时间 |
enableKeepAlive |
false | 高并发场景设 true,TCP keep-alive |
七、连接管理最佳实践
javascript:mysql-best.js
const pool = mysql.createPool({ /* config */ });
// ✅ 推荐:通过 pool 自动管理
const [rows] = await pool.query('SELECT ...');
// ❌ 不推荐:手动创建/关闭连接
const conn = await mysql.createConnection({ /* config */ });
await conn.query('SELECT ...');
await conn.end(); // 容易忘记关闭导致连接泄漏
::: warning
连接泄漏 ------忘记 connection.end() 是最常见的资源泄漏。每次泄漏一个连接,MySQL SHOW PROCESSLIST 会看到一堆 Sleep 状态的连接,最终 max_connections 满。
:::
八、事务
javascript:mysql-transaction.js
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
await conn.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]);
await conn.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, 2]);
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release(); // 归还连接到池
}
注意:getConnection() 拿到的连接用完必须 release()(不是 end())。end() 会销毁连接,而 release() 只是归还到池。
九、常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
ECONNREFUSED |
连接被拒(端口 / 防火墙) | 检查 MySQL 监听端口、防火墙 |
ER_ACCESS_DENIED_ERROR |
密码错 | 重置 root 密码 |
ER_BAD_DB_ERROR |
数据库不存在 | CREATE DATABASE 或改 database 字段 |
PROTOCOL_CONNECTION_LOST |
连接断了 | 启用 enableKeepAlive: true |
ER_DUP_ENTRY |
唯一键冲突 | 检查数据或用 INSERT ... ON DUPLICATE KEY UPDATE |
| 连接池耗尽(卡死) | 太多未释放的连接 | 用 pool.query 而非手动 getConnection |
十、小结
mysql2是新项目首选驱动;性能更好、Promise 原生支持、兼容 MySQL 8- 永远用连接池 (
createPool),避免每次查询都建/拆连接 - 永远用占位符 (
?),不要拼接 SQL------防注入是底线性要求 execute()走预处理协议,比query()更快更安全- 事务用
getConnection()+beginTransaction/commit/rollback,用完必须release() - 连接管理不当会导致连接泄漏;监控
SHOW PROCESSLIST看 Sleep 状态的连接数