Node.js 搭配 MySQL 是經典的全端開發組合。早期常用 db-mysql,但因依賴 node-waf 已不可用。目前主流推薦官方維護的 mysql2 (或 mysql) 套件,純 JavaScript 實現、MIT 授權、效能優異。本文基於官方文檔整理完整使用指南。
npm install mysql2
# 或 npm install mysql
const mysql = require('mysql2');
const connection = mysql.createConnection({
host: 'localhost',
user: 'me',
password: 'secret',
database: 'my_db'
});
// 連線並處理錯誤
connection.connect((err) => {
if (err) {
console.error('連線失敗:', err.stack);
return;
}
console.log('已連線,threadId:', connection.threadId);
});
// 查詢範例
connection.query('SELECT 1 + 1 AS solution', (err, rows, fields) => {
if (err) throw err;
console.log('結果:', rows[0].solution);
});
// 優雅關閉
connection.end((err) => { /* 連線已終止 */ });
| 選項 | 說明 | 預設值 |
|---|---|---|
| host | 資料庫主機名 | localhost |
| port | 連線埠口 | 3306 |
| socketPath | Unix Socket 路徑 (優先於 host/port) | - |
| user/password | 認證憑證 | - |
| database | 預設資料庫 | - |
| charset | 字元集 (SQL 層面稱 collation) | UTF8_GENERAL_CI |
| timezone | 日期儲存時區 | local |
| connectTimeout | 連線逾時毫秒數 | 10000 |
| supportBigNumbers | 支援 BIGINT/DECIMAL 大數值 | false |
| bigNumberStrings | 大數值以字串返回 (需配合 supportBigNumbers) | false |
| dateStrings | 日期型別以字串返回而非 Date 物件 | false |
| multipleStatements | 允許單次查詢多條 SQL (防注入風險) | false |
| ssl | SSL 配置物件或 profile 名稱 | - |
const connection = mysql.createConnection(
'mysql://user:pass@host/db?debug=true&charset=BIG5_CHINESE_CI&timezone=-0700'
);
生產環境強烈建議使用連線池,自動管理連線生命週期:
const pool = mysql.createPool({
connectionLimit: 10,
host: 'example.org',
user: 'bob',
password: 'secret',
database: 'my_db'
});
// 直接查詢 (自動取用/釋放連線)
pool.query('SELECT 1+1 AS solution', (err, rows) => {
if (err) throw err;
console.log(rows[0].solution);
});
// 手動取用連線 (需手動釋放)
pool.getConnection((err, connection) => {
if (err) throw err;
connection.query('SELECT * FROM sometable', (err, rows) => {
connection.release(); // 歸還連線池
if (err) throw err;
console.log(rows);
});
});
// 關閉連線池
pool.end((err) => { /* 所有連線已關閉 */ });
| 選項 | 說明 | 預設值 |
|---|---|---|
| acquireTimeout | 取得連線逾時 | 10000 |
| waitForConnections | 滿載時排隊等待 vs 立即報錯 | true |
| connectionLimit | 最大連線數 | 10 |
| queueLimit | 排隊上限 (0=無限制) | 0 |
pool.on('connection', (conn) => {
conn.query('SET SESSION auto_increment_increment=1');
});
pool.on('enqueue', () => console.log('等待可用連線...'));
// 佔位符 ? 陣列對應
connection.query('SELECT * FROM books WHERE author = ?', ['David'], callback);
// 物件對應 (自動轉義鍵值)
const post = {id: 1, title: 'Hello MySQL'};
connection.query('INSERT INTO posts SET ?', post, callback);
// 產生: INSERT INTO posts SET `id`=1, `title`='Hello MySQL'
// 識別符轉義 (資料表/欄位名)
const sorter = 'date';
connection.query('SELECT * FROM posts ORDER BY ??', [sorter], callback);
// 產生: SELECT * FROM posts ORDER BY `date`
connection.query({
sql: 'SELECT * FROM books WHERE author = ?',
timeout: 40000,
values: ['David']
}, callback);
const sql = 'SELECT * FROM ?? WHERE ?? = ?';
const inserts = ['users', 'id', userId];
const formatted = mysql.format(sql, inserts);
// 產生安全查詢字串
connection.config.queryFormat = (query, values) => {
if (!values) return query;
return query.replace(/:(\w+)/g, (txt, key) => {
if (values.hasOwnProperty(key)) return connection.escape(values[key]);
return txt;
});
};
connection.query('UPDATE posts SET title = :title', {title: 'Hello MySQL'});
connection.query('INSERT INTO posts SET ?', {title: 'test'}, (err, result) => {
if (err) throw err;
console.log('新增 ID:', result.insertId);
});
// 影響行數 (含未實際變更的符合條件行)
connection.query('DELETE FROM posts WHERE title="wrong"', (err, result) => {
console.log('刪除行數:', result.affectedRows);
});
// 實際變更行數 (排除值未改變的更新)
connection.query('UPDATE posts SET ...', (err, result) => {
console.log('變更行數:', result.changedRows);
});
const query = connection.query('SELECT * FROM huge_table');
query.on('fields', (fields) => { /* 欄位元資料 */ })
.on('result', (row) => {
connection.pause(); // 暫停接收,處理 I/O 密集任務
processRow(row, () => connection.resume());
})
.on('end', () => { /* 完成 */ })
.on('error', (err) => { /* 錯誤處理 */ });
// 或使用 Stream2 管道
connection.query('SELECT * FROM posts')
.stream({highWaterMark: 5, objectMode: true})
.pipe(writableStream);
const connection = mysql.createConnection({multipleStatements: true});
connection.query('SELECT 1; SELECT 2', (err, results) => {
console.log(results[0]); // [{1: 1}]
console.log(results[1]); // [{2: 2}]
});
呼叫方式與一般查詢相同,若返回多結果集,結構同多語句查詢。
// 巢狀結構
const options = {sql: '...', nestTables: true};
// 結果: [{table1: {fieldA:...}, table2: {fieldA:...}}]
// 分隔符合併
const options = {sql: '...', nestTables: '_'};
// 結果: [{table1_fieldA:..., table2_fieldA:...}]
connection.beginTransaction((err) => {
if (err) throw err;
connection.query('INSERT INTO posts SET title=?', [title], (err, result) => {
if (err) return connection.rollback(() => { throw err; });
const log = 'Post ' + result.insertId + ' added';
connection.query('INSERT INTO log SET data=?', [log], (err, result) => {
if (err) return connection.rollback(() => { throw err; });
connection.commit((err) => {
if (err) return connection.rollback(() => { throw err; });
console.log('交易成功!');
});
});
});
});
// Ping 檢查
connection.ping((err) => { if (err) throw err; console.log('伺服器回應'); });
// 查詢層級逾時 (60秒)
connection.query({sql: 'SELECT COUNT(*) FROM big_table', timeout: 60000}, (err, rows) => {
if (err && err.code === 'PROTOCOL_SEQUENCE_TIMEOUT')
throw new Error('查詢逾時!');
console.log(rows[0].count);
});
err.code (MySQL 錯誤如 ER_ACCESS_DENIED_ERROR、Node 錯誤如 ECONNREFUSED、內部錯誤如 PROTOCOL_CONNECTION_LOST)err.fatal=true 表示連線已不可用,會傳播給所有待決回調connection.on('error', ...)| JS 型別 | MySQL 型別 |
|---|---|
| Number | TINYINT, SMALLINT, INT, MEDIUMINT, YEAR, FLOAT, DOUBLE |
| Date | TIMESTAMP, DATE, DATETIME |
| Buffer | TINYBLOB, MEDIUMBLOB, LONGBLOB, BLOB, BINARY, VARBINARY, BIT |
| String | CHAR, VARCHAR, TINYTEXT, MEDIUMTEXT, LONGTEXT, TEXT, ENUM, SET, DECIMAL, BIGINT, TIME, GEOMETRY |
可透過 typeCast: false 關閉自動轉換,或自訂 typeCast 函數 (如將 TINYINT(1) 轉 Boolean)。
// 開啟除錯模式
const connection = mysql.createConnection({debug: true});
// 或限定封包類型
const connection = mysql.createConnection({debug: ['ComQueryPacket', 'RowDataPacket']});
單元測試:FILTER=unit npm test;整合測試需設定環境變數 MYSQL_HOST、MYSQL_DATABASE 等後執行 npm test。