背景
在Egg開發實踐中,經常會遇到一個問題:如何查看剛剛執行過的Egg組裝的原生SQL語句呢?
1. 現有方案
可以直接在項目的config配置文件中添加MySQL配置debug: true
。這會啓用底層模塊mysql
的調試標誌,然後輸出有關SQL語句的詳盡信息,效果如下:
2. 弊端
debug: true
方案有如下弊端:
-
輸出信息過於詳細,在實際開發中反而會干擾我們快速查看其他日誌信息
-
沒有輸出SQL語句的執行時間
3. 理想方案
對於一個理想的SQL語句輸出方案,我們其實只關心兩個問題:
- Egg組裝的原生SQL語句到底是怎樣的?便於我們快速排查問題
- SQL語句的執行時間是多少,便於我們儘早鎖定性能問題,從而得到及時解決
4. CabloyJS的方案效果
CabloyJS是基於Egg的上層開發框架,針對前面提到的兩個核心問題,實現瞭如下效果
這種SQL語句日誌的輸出效果:不僅一目瞭然可以看到剛剛執行了多少SQL語句,而且每一條SQL語句的執行時間也是歷歷在目。當然,順便我們還能看到SQL語句是由哪個連接對象執行的(通過threadId
)
實現方案
下面我們看一下CabloyJS是如何實現的。這種實現機制也適用於其他Egg系的上層框架
假設你已經創建了一個CabloyJS的項目,下面的源碼均位於CabloyJS項目內
如何創建CabloyJS項目,請參見:快速開始
1. config定義
爲了讓方案更靈活,我們先擴展一下MySQL參數的定義
node_modules/egg-born-backend/config/config.default.js
// mysql
config.mysql = {
clients: {
__ebdb: {
// debug: true,
hook: {
meta: {
color: 'orange',
long_query_time: 0,
},
callback: {
onQuery,
},
},
},
},
};
名稱 | 說明 |
---|---|
debug | 如果爲true,就是啓用內置的調試標誌。在這裏沒有啓用 |
hook.meta | 包含hook的配置參數 |
hook.meta.color | 日誌輸出的顏色 |
hook.meta.long_query_time | 如果SQL語句的執行時間超過了long_query_time (ms),就會被輸出到控制檯。特別的,如果long_query_time 設爲0 ,則輸出所有SQL語句 |
hook.callback.onQuery | 爲了提升靈活性,我們可以通過onQuery 提供一個自定義的回調函數,當SQL語句的執行信息準備就緒時會被自動調用 |
2. 改寫模塊ali-rds
Egg執行MySQL語句的技術棧如下:模塊egg
-> 模塊egg-mysql
-> 模塊ali-rds
-> 模塊mysql
在這裏,我們只需要改寫模塊ali-rds
即可
node_modules/@zhennann/ali-rds/lib/client.js
function RDSClient(options) {
if (!(this instanceof RDSClient)) {
return new RDSClient(options);
}
Operator.call(this);
this.pool = mysql.createPool(options);
const _hook = options.hook;
const _getConnection = this.pool.getConnection.bind(this.pool);
this.pool.getConnection = function(cb) {
_getConnection(function(err, conn) {
if (err) return cb(err, null);
onQuery(conn, function(err) {
if (err) return cb(err, null);
onConnection(conn, function(err) {
if (err) return cb(err, null);
cb(null, conn);
});
});
});
function onConnection(conn, cb) {
if (!_hook || !_hook.callback || !_hook.callback.onConnection) return cb(null);
if (conn.__hook_onConnection) return cb(null);
conn.__hook_onConnection = true;
co.wrap(_hook.callback.onConnection)(new RDSConnection(conn)).then(function() {
cb(null);
}).catch(function(err) {
cb(err);
});
}
function onQuery(conn, cb) {
if (!_hook || !_hook.callback || !_hook.callback.onQuery) return cb(null);
if (conn.__hook_onQuery) return cb(null);
conn.__hook_onQuery = true;
const _query = conn.query;
conn.query = function query(sql, values, cb) {
const prevTime = Number(new Date());
const sequence = _query.call(conn, sql, values, cb);
const _callback = sequence._callback;
sequence._callback = function(...args) {
const ms = Number(new Date()) - prevTime;
_hook.callback.onQuery(_hook, ms, sequence, args);
_callback && _callback(...args);
};
return sequence;
};
cb(null);
}
};
[
'query',
'getConnection',
].forEach(method => {
this.pool[method] = promisify(this.pool[method]);
});
}
-
首先,攔截
pool.getConnection
方法 -
當系統從數據庫連接池中獲取到connection對象時,執行兩個回調
onConnection
和onQuery
-
onConnection
是在第一次創建connection對象時,執行一些初始化SQL語句,比如設置一些會話級別的變量,不是這裏討論的重點 -
onQuery
的作用就是攔截connection.query
方法,在query執行前和執行後分別記錄時間,從而得到SQL語句的執行時間,然後執行config配置中指定的回調函數hook.callback.onQuery
3. 回調hook.callback.onQuery
我們再回頭看一下config配置文件中的回調函數是如何實現的
node_modules/egg-born-backend/config/config.default.js
function onQuery(hook, ms, sequence, args) {
if (!hook.meta.long_query_time || hook.meta.long_query_time < ms) {
const message = `threadId: ${sequence._connection.threadId}, ${ms}ms ==> ${sequence.sql}`;
console.log(chalk.keyword(hook.meta.color)(message));
}
}
-
首先判斷
hook.meta.long_query_time
,如果爲0
或者小於執行時間,就會執行輸出 -
使用模塊
chalk
,並使用指定的顏色值hook.meta.color
輸出SQL執行日誌
4. 模塊module-alias
由於我們改寫了模塊ali-rds
的源代碼,所以我們需要啓用一個新的模塊名稱,在這裏就是@zhennann/ali-rds
,發佈到npm倉庫即可
那麼,如何使新模塊@zhennann/ali-rds
生效呢?由於模塊ali-rds
是被模塊egg-mysql
所引用的。我們如果還要改寫模塊egg-mysql
的源碼,代價就未免太大了
在這裏,我們引入模塊module-alias
,從而達到這樣的效果:模塊egg-mysql
源碼不變,仍然是const rds = require('ali-rds');
,但實際上引用的卻是@zhennann/ali-rds
模塊
module-alias
的機理,請參見:https://github.com/ilearnio/module-alias
這裏,我們只需看一下如何使用模塊module-alias
node_modules/egg-born-backend/index.js
const moduleAlias = require('module-alias');
moduleAlias.addAlias('ali-rds', '@zhennann/ali-rds');
結語
這樣,我們就實現了一個輕巧的方案,不僅可以直接在Egg上層框架中提供缺省的SQL語句輸出方案,而且還可以通過覆蓋config參數hook.callback.onQuery
提供自定義的輸出方案