关于使用Sequelize进行数据库连接/释放方面的解惑

1,664 阅读5分钟

写这个文章的目的

最近想学习下node语言下的orm框架,于是选择了Sequelize,他的中文文档很详细,网上相关的文章也比较多,但是,没有找到明确的关于数据库的连接与释放的内容,毕竟数据库连接很重要,如果你不知道数据库的连接是什么时候获取的,什么时候释放的,你程序再精妙也会让你有种不踏实的感觉。为了消除这种不踏实的感觉,因此写下这篇文章进行记录。

也就是说我需要消除这2点疑惑

  1. Sequelize的数据库连接是什么时候获取和释放的
  2. new Sequelize()对象是否可以全局共用一个还是说每次使用时都需要new Sequelize()

Sequelize是什么

node语言下用于数据库开发的ORM框架

数据库开发相关概念

  • 数据库
  • 数据库连接
  • 数据库连接池
  • 字段
  • 约束与索引

Sequelize为我们提供了哪些便利

进行数据库编程,大概需要经历这7个步骤

  1. 建库和建表
  2. 配置数据库连接信息
  3. 建立数据库连接或数据库连接池
  4. 拿到数据库连接然后开启事务
  5. 执行数据库操作相关代码(如: select xxx)
  6. 提交事务
  7. 关闭数据库连接或将数据库连接归还到数据库连接池

用例Sequelize之后,如何进行数据库编程

  1. 建表:通过 sequelize.define(xxx)自动建表
  2. 配置数据库连接信息:通过 const sequelize = new Sequelize(...) 配置数据库连接信息
  3. 建立数据库连接或数据库连接池:sequelize在你执行数据库操作语句之前,自动帮你创建以及获取连接 [这一步是自动完成的,开发者无需考虑]
  4. 拿到数据库连接然后开启事务:sequelize有相关方法支持
  5. 执行数据库操作相关代码(如: select xxx):sequelize有相关方法支持
  6. 提交事务:sequelize在你的sql代码执行完成之后,自动帮你提交事务 [这一步是自动完成的,开发者无需考虑]
  7. 关闭数据库连接或将数据库连接归还到数据库连接池: [这一步是自动完成的,开发者无需考虑]

标注了这句话的[这一步是自动完成的,开发者无需考虑],3,6,7条,你有啥证明吗?不会是我yy出来的吧,有种不踏实的感觉,那么就必须从源码中找证据了

构造方法节选

export class Sequelize {
   constructor(database, username, password, options) {
       // ... 省略一堆与本次问题不那么相关的代码
       let Dialect;
       // ... 省略一堆与本次问题不那么相关的代码
       switch (this.getDialect()) {
           case 'mariadb':
               Dialect = require('./dialects/mariadb').MariaDbDialect;
               break;
           case 'mssql':
               Dialect = require('./dialects/mssql').MssqlDialect;
               break;
           case 'mysql':
               Dialect = require('./dialects/mysql').MysqlDialect;
               break;
           case 'postgres':
               Dialect = require('./dialects/postgres').PostgresDialect;
               break;
           case 'sqlite':
               Dialect = require('./dialects/sqlite').SqliteDialect;
               break;
           case 'ibmi':
               Dialect = require('./dialects/ibmi').IBMiDialect;
               break;
           case 'db2':
               Dialect = require('./dialects/db2').Db2Dialect;
               break;
           case 'snowflake':
               Dialect = require('./dialects/snowflake').SnowflakeDialect;
               break;
           default:
               throw new Error(`The dialect ${this.getDialect()} is not supported. Supported dialects: mariadb, mssql, mysql, postgres, sqlite, ibmi, db2 and snowflake.`);
       }
       this.dialect = new Dialect(this);
       // ... 省略一堆与本次问题不那么相关的代码

       /**
        * Models are stored here under the name given to `sequelize.define`
        */
       this.models = {};
       this.modelManager = new ModelManager(this);
       // 连接的获取与释放都是由connectionManager完成, 从后续的query代码节选中就可以看到connectionManager的使用
       this.connectionManager = this.dialect.connectionManager;

       Sequelize.runHooks('afterInit', this);
   }
}

从github仓库可以看到 sequelize源码, 查看 sequelize.js的query方法源码

query方法代码节选

async query(sql, options) {

   // ... 省略一堆对本次问题不重要的代码

   // 定义检查事务的方法
   const checkTransaction = () => {
     if (options.transaction && options.transaction.finished && !options.completesTransaction) {
       const error = new Error(`${options.transaction.finished} has been called on this transaction(${options.transaction.id}), you can no longer use it. (The rejected query is attached as the 'sql' property of this error)`);
       error.sql = sql;
       throw error;
     }
   };

   // 定义重试配置
   const retryOptions = { ...this.options.retry, ...options.retry };

   // 执行重试方法,并将结果返回
   return retry(async () => {
     if (options.transaction === undefined && Sequelize._cls) {
       options.transaction = Sequelize._cls.get('transaction');
     }

     // 检查事务
     checkTransaction();

     // 获取连接:如果存在事务则获取事务里的连接,如果不存在事务,则从connectionManager中获取
     // ************************ 这段代码就是自动帮我们获取数据库连接
     const connection = await (options.transaction ? options.transaction.connection : this.connectionManager.getConnection({
       useMaster: options.useMaster,
       type: options.type === 'SELECT' ? 'read' : 'write',
     }));

     if (this.options.dialect === 'db2' && options.alter && options.alter.drop === false) {
       connection.dropTable = false;
     }

     const query = new this.dialect.Query(connection, this, options);

     try {
       await this.runHooks('beforeQuery', options, query);
       checkTransaction();
       // 执行sql语句并返回结果
       return await query.run(sql, bindParameters);
     } finally {
       await this.runHooks('afterQuery', options, query);
       if (!options.transaction) {
         // 释放数据库连接
         // ************************ 这段代码就是自动帮我们释放数据库连接
         await this.connectionManager.releaseConnection(connection);
       }
     }
   }, retryOptions);
 }

Sequelize文档中有提到close方法,那这是多此一举?

close方法用于关闭所有数据库连接。等等,不对劲,你上面不是说数据库的连接与关闭都是Sequelize自动完成的吗?为啥这里又搞个方法用于手动关闭,并且还是关闭所有的数据库连接?

从上面的节选代码中可以看出,实际的数据库连接获取与释放是由connectionManager内部完成的。当我们使用了数据库连接池时,connectionManager.releaseConnection(connection) 方法内部,可能只是将连接归还了连接池,实际并没有真正意义的关闭。但我们的数据库连接最终肯定是要真正关闭的。

所以,这就是close方法的作用,就是在你的服务关闭或应用退出时,通过此方法,将数据库连接全部正常关闭

close方法源码

/**
 * Close all connections used by this sequelize instance, and free all references so the instance can be garbage collected.
 *
 * Normally this is done on process exit, so you only need to call this method if you are creating multiple instances, and want
 * to garbage collect some of them.
 *
 * @returns {Promise}
 */
close() {
  return this.connectionManager.close();
}

总结

到这里,一开始抛出的两个疑问已经有了结论

  1. Sequelize的数据库连接是什么时候获取和释放的 在执行Sequelize提供的数据库操作的相关方法中,方法内部会自动帮你获取/释放数据库连接资源

  2. new Sequelize()对象是否可以全局共用一个还是说每次使用时都需要new Sequelize() 之所以有这个疑问还是因为将new Sequelize(xxx)的代码,看作了java中的new Connection()代码,所以对Sequelize对象能否全局公用一个存有怀疑,但从上面的源码中可以得知,new Sequelize并不是java中new Connection(),因此完全可以全局使用同一个Sequelize对象,并且只能全局共用一个Sequelize对象,不然就会产生多个connectionManager, 数据库连接数量可能与你的预期不符,且事务控制也可能出现问题