背景

在Egg开发实践中,经常会遇到一个问题:如何查看刚刚执行过的Egg组装的原生SQL语句呢?

1. 现有方案

可以直接在项目的config配置文件中添加MySQL配置debug: true。这会启用底层模块mysql的调试标志,然后输出有关SQL语句的详尽信息,效果如下:

2. 弊端

debug: true方案有如下弊端:

  1. 输出信息过于详细,在实际开发中反而会干扰我们快速查看其他日志信息

  2. 没有输出SQL语句的执行时间

3. 理想方案

对于一个理想的SQL语句输出方案,我们其实只关心两个问题:

  1. Egg组装的原生SQL语句到底是怎样的?便于我们快速排查问题
  2. 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

  1. // mysql
  2. config.mysql = {
  3. clients: {
  4. __ebdb: {
  5. // debug: true,
  6. hook: {
  7. meta: {
  8. color: 'orange',
  9. long_query_time: 0,
  10. },
  11. callback: {
  12. onQuery,
  13. },
  14. },
  15. },
  16. },
  17. };
名称 说明
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

  1. function RDSClient(options) {
  2. if (!(this instanceof RDSClient)) {
  3. return new RDSClient(options);
  4. }
  5. Operator.call(this);
  6. this.pool = mysql.createPool(options);
  7. const _hook = options.hook;
  8. const _getConnection = this.pool.getConnection.bind(this.pool);
  9. this.pool.getConnection = function(cb) {
  10. _getConnection(function(err, conn) {
  11. if (err) return cb(err, null);
  12. onQuery(conn, function(err) {
  13. if (err) return cb(err, null);
  14. onConnection(conn, function(err) {
  15. if (err) return cb(err, null);
  16. cb(null, conn);
  17. });
  18. });
  19. });
  20. function onConnection(conn, cb) {
  21. if (!_hook || !_hook.callback || !_hook.callback.onConnection) return cb(null);
  22. if (conn.__hook_onConnection) return cb(null);
  23. conn.__hook_onConnection = true;
  24. co.wrap(_hook.callback.onConnection)(new RDSConnection(conn)).then(function() {
  25. cb(null);
  26. }).catch(function(err) {
  27. cb(err);
  28. });
  29. }
  30. function onQuery(conn, cb) {
  31. if (!_hook || !_hook.callback || !_hook.callback.onQuery) return cb(null);
  32. if (conn.__hook_onQuery) return cb(null);
  33. conn.__hook_onQuery = true;
  34. const _query = conn.query;
  35. conn.query = function query(sql, values, cb) {
  36. const prevTime = Number(new Date());
  37. const sequence = _query.call(conn, sql, values, cb);
  38. const _callback = sequence._callback;
  39. sequence._callback = function(...args) {
  40. const ms = Number(new Date()) - prevTime;
  41. _hook.callback.onQuery(_hook, ms, sequence, args);
  42. _callback && _callback(...args);
  43. };
  44. return sequence;
  45. };
  46. cb(null);
  47. }
  48. };
  49. [
  50. 'query',
  51. 'getConnection',
  52. ].forEach(method => {
  53. this.pool[method] = promisify(this.pool[method]);
  54. });
  55. }
  1. 首先,拦截pool.getConnection方法

  2. 当系统从数据库连接池中获取到connection对象时,执行两个回调onConnectiononQuery

  3. onConnection是在第一次创建connection对象时,执行一些初始化SQL语句,比如设置一些会话级别的变量,不是这里讨论的重点

  4. onQuery的作用就是拦截connection.query方法,在query执行前和执行后分别记录时间,从而得到SQL语句的执行时间,然后执行config配置中指定的回调函数hook.callback.onQuery

3. 回调hook.callback.onQuery

我们再回头看一下config配置文件中的回调函数是如何实现的

node_modules/egg-born-backend/config/config.default.js

  1. function onQuery(hook, ms, sequence, args) {
  2. if (!hook.meta.long_query_time || hook.meta.long_query_time < ms) {
  3. const message = `threadId: ${sequence._connection.threadId}, ${ms}ms ==> ${sequence.sql}`;
  4. console.log(chalk.keyword(hook.meta.color)(message));
  5. }
  6. }
  1. 首先判断hook.meta.long_query_time,如果为0或者小于执行时间,就会执行输出

  2. 使用模块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

  1. const moduleAlias = require('module-alias');
  2. moduleAlias.addAlias('ali-rds', '@zhennann/ali-rds');

结语

这样,我们就实现了一个轻巧的方案,不仅可以直接在Egg上层框架中提供缺省的SQL语句输出方案,而且还可以通过覆盖config参数hook.callback.onQuery提供自定义的输出方案

Egg上层框架CabloyJS是如何输出SQL语句日志的?的更多相关文章

  1. yii2输出sql语句

    yii2如何输出具体的查询的sql语句: $query = User::find() ->where(['id'=>[1,2,3,4]) ->select(['username']) ...

  2. IBatis.net 输出SQL语句(七)

    一.IBatis.net输出SQL语句到控制台 输出IBatis.net生成的SQL语句到控制台,能够方便调试. 如果要想输出IBatis.net的SQL语句到控制台,那么只需要做如下配置即可: &l ...

  3. NHibernate输出SQL语句

    用了NHierbate之后,很少需要写原生的SQL语句,由于总是看不到SQL语句,所以有时候对SQL调优非常不利.因此产生了让NHibernate输出它所生成的SQL语句的想法,以便于后续调优. 一. ...

  4. Spring3+MyBatis3整合log4j无法输出SQL语句问题的解决

    今天遇到了跟下面文章一模一样的问题,下面文章的解决方案很好,在这里记录保存一下. Spring3+MyBatis3整合无法输出SQL语句问题的解决

  5. Ibatis.Net 输出SQL语句学习(七)

    一.IBatis.net输出SQL语句 输出IBatis.net生成的SQL语句,能够方便调试. 在MapperHelper类中添加GetSql方法: /// <summary> /// ...

  6. django框架 - 实时查看执行的sql语句

    django框架采用的ORM模型,我们可以通过mysql的日志记录实时看到执行的sql语句,具体步骤如下: 第一步:找到mysql的配置文件 第二步:编辑mysql配置文件 第三步:重启mysql 第 ...

  7. Druid搭配log4j2输出SQL语句和结果

    一.引言 其实Druid的内置了log4jdbc来显示SQL语句,虽然显示效果不如原生的log4jdbc效果好,但是因为内置所以不需要其他更多的配置. 二.使用 1. 创建基于druid的logger ...

  8. 配置p6spyLog输出sql完整日志

      第一步:   配置maven <dependency> <groupid>p6spy</groupid> <artifactid>p6spy< ...

  9. Yii 2.0版本调试输出SQL语句

    项目是基于框架Yii 2.0开发的. 今天梳理一些数据统计功能代码的时候,想把当前运行的sql语句打印出来,然后放到navicat工具里面运行,并分析一下运行效率和调优方案,之前大部分时候都是写增加. ...

随机推荐

  1. EL表达式详解(常用表达式以及取值)

    EL表达式 学习总结 一. El表达式概念 二. El中的表达式 1. 算术表达式 2. 比较表达式 3. 逻辑表达式 4. 三元表达式 5. 判空表达式 三.EL 从四个作用域中取值 1. 概念 2 ...

  2. Java中JSONArray转换成int[]的办法

    今天写项目的时候要做一个MyBatis的带IN子句的删除,于是用一个整型数组来保存待删除数据的ID 从前端将JSON字符串搞过来之后如何将JSONArray转换成int类型数组就成了个问题 下面是我的 ...

  3. JavaScript实现科学计算器

    运行效果: 可实现科学计算器的功能,如:PI,sin,cos,tan等 源代码: 1 <!DOCTYPE html> 2 <html lang="zh"> ...

  4. Wireshark捕获网易云音乐音频文件地址

    打开Wireshark,开始捕获. 打开网易云音乐,然后播放一首歌. Wireshark停时捕获,然后在不活的文件中搜索字符串"mp3".可以发现有如下信息: 将其中的内容:&qu ...

  5. 详解防抖函数(debounce)和节流函数(throttle)

    本文转自:https://www.jianshu.com/p/f9f6b637fd6c 闭包的典型应用就是函数防抖和节流,本文详细介绍函数防抖和节流的应用场景和实现. 函数防抖(debounce) 函 ...

  6. redis5.0.0集群搭建【实战经历】

    redis集群搭建 作者:陈土锋 时间:2020年6月2日 目录 一.环境介绍... 1 1.机器准备... 1 2.关闭防护墙和selinux. 1 3.时间同步... 1 二.Redis Clus ...

  7. git设置忽略提交文件

    直接在idea进行操作 1.找到想要忽略提交的文件,点击右键,然后如下操作: 2.然后就会发现被忽略的文件名变成了灰色,在项目最下方会生成ignore文件夹 3.文件夹内可以看到我们忽略的文件 4.注 ...

  8. 定时执行任务-springboot

    定时执行任务-springboot 先看两个接口 这两个接口springboot已经帮我们封装好了,我们不需要去手动使用 TaskScheduler //任务调度者 TaskExecutor //任务 ...

  9. GRPC-go版本

    GRPC-go版本 1.安装GO,protobuf 只适合有梯子的 GO的安装没必要说了 protobuf :https://github.com/protocolbuffers/protobuf/r ...

  10. 开发中常用的Hook

    开发中常用的Hook 什么是Hook? Hook 是一些可以让你在函数组件里"钩入" React state 及生命周期等特性的函数,用来实现一些 class 组件的特性的. 1 ...