02|连接配置、连接池与环境变量

数据库连接不是“每执行一次 SQL 就 new 一个连接”。在 Web 服务中,Sequelize 通常持有一个进程级实例,由它管理连接池。

1. 当前工程如何创建 Sequelize 实例

db.ts 的核心结构是:

const db = new Sequelize({
  database: getEnv("DB_NAME"),
  host: getEnv("DB_HOST"),
  username: getEnv("DB_USER"),
  password: getEnv("DB_PASSWORD"),
  dialect: "mysql",
  dialectOptions: {
    supportBigNumbers: true,
    bigNumberStrings: true,
  },
  define: {
    charset: "utf8mb4",
    collate: "utf8mb4_0900_ai_ci",
  },
});

然后导出同一个实例:

export default db;

其他 Model 和 Service 导入它,而不是各自创建新实例。

2. 配置项解决什么问题

dialect

dialect: "mysql"

告诉 Sequelize 使用 MySQL 方言和对应驱动。本项目安装的是 mysql2。Sequelize 负责 ORM,mysql2 负责协议通信。

连接地址和凭据

database: getEnv("DB_NAME"),
host: getEnv("DB_HOST"),
username: getEnv("DB_USER"),
password: getEnv("DB_PASSWORD"),

敏感值来自环境变量而不是源码。getEnv() 使缺失配置尽早失败,避免把 undefined 静默传入连接层。

环境变量仍然只是字符串。端口、连接池大小等数字配置要严格转换并校验:

function getPositiveIntegerEnv(key: string): number {
  const raw = getEnv(key);
  const value = Number(raw);

  if (!Number.isInteger(value) || value <= 0) {
    throw new Error(`环境变量 ${key} 必须是正整数`);
  }

  return value;
}

不要使用会接受尾部垃圾字符的 parseInt("10abc", 10)。

define

define: {
  charset: "utf8mb4",
  collate: "utf8mb4_0900_ai_ci",
}

这是 Model 定义的默认配置,主要影响 Sequelize 创建表时的默认值。它不会修改一个已存在表的字符集或排序规则。真实表结构仍应通过 DDL/Migration 管理。

utf8mb4_0900_ai_ci 中:

因此字符串唯一性也可能不区分大小写。Admin 与 admin 是否冲突,最终以列/索引采用的排序规则为准。

3. BIGINT 为什么必须特殊处理

MySQL BIGINT UNSIGNED 最大值远大于 JavaScript 安全整数:

Number.MAX_SAFE_INTEGER; // 9007199254740991

当前配置:

dialectOptions: {
  supportBigNumbers: true,
  bigNumberStrings: true,
}

要求驱动把大整数按字符串交给 JavaScript。Model 因此声明:

declare id: CreationOptional<string>;

正确的数据链路:

MySQL BIGINT → mysql2 string → Model string → DTO/JWT string

错误做法:

String(already_rounded_number)

如果驱动先返回 number 并已丢失精度,String() 只能把错误的数值字符串化,无法恢复原值。

4. 连接池是什么

建立数据库连接需要握手和认证,成本不应由每个请求重复承担。连接池维护一组可复用连接:

请求 A ─┐
请求 B ─┼→ Sequelize pool → MySQL connections
请求 C ─┘

可显式配置:

const db = new Sequelize({
  // ...
  pool: {
    max: 10,
    min: 0,
    acquire: 30_000,
    idle: 10_000,
  },
});

这些不是越大越好。多个应用实例的连接池总和不能超过数据库承受能力。

5. 为什么整个进程只创建一个实例

错误示例:

export async function queryAdmin() {
  const db = new Sequelize(/* ... */);
  return AdminUser.findAll();
}

每个请求创建实例会产生多个连接池,导致:

Web 服务应在启动时创建一次,所有请求共享。

6. authenticate() 做了什么

await db.authenticate();

它验证当前配置能否连接和执行简单查询。日志中常见:

SELECT 1+1 AS result;

它不做以下事情:

因此启动成功只说明此刻基础连接可用。

7. close() 应在什么时候调用

一次性 CLI 脚本:

try {
  await run();
} finally {
  await db.close();
}

脚本完成后必须释放连接池,否则 Node.js 可能因仍有活动连接而不退出。

Web 服务则不同:不能在每次请求后 close()。应在整个进程准备退出时统一关闭,例如处理优雅停机信号。

如果 CLI 和 Web 服务是两个 Node.js 进程,各自导入 db.ts 会各自创建自己的内存实例与连接池。CLI 的 db.close() 不会关闭另一个进程的池。

8. process.cwd() 与 .env

import "dotenv/config" 默认基于当前工作目录寻找 .env。所以:

# cwd 是项目根目录
ts-node .\scripts\create-initial-admin.ts

# cwd 是 scripts 目录
cd scripts
ts-node create-initial-admin.ts

两种执行方式加载配置的位置可能不同。推荐:

不要在库模块中随意改变 process.cwd()。

9. SQL 日志如何使用

Sequelize 的 logging 可以是布尔值或函数:

const db = new Sequelize({
  // ...
  logging: (sql) => {
    console.debug({ event: "sequelize_query", sql });
  },
});

开发时观察 SQL 很有价值,但要注意:

当前控制台出现的 Executing (default): ... 是 Sequelize 默认 SQL 日志。它一般写到进程输出,再由运行环境、PM2、Docker 或日志系统收集。

10. 开发、测试、生产配置

至少应区分:

不要用 NODE_ENV 之外的模糊字符串隐式决定高风险行为,更不要在生产配置下运行 sync({ force: true })。

11. 初学者常见错误

  1. 每个 Service 创建 Sequelize 实例,造成多个连接池。
  2. 每次请求后调用 db.close(),下一个请求报连接管理器已关闭。
  3. 把 .env 提交到 Git,泄露凭据。
  4. 认为 define.collate 会自动修改现有表。
  5. 把 BIGINT 声明成 number,产生静默精度损失。
  6. 把连接池 max 设置得极大,忽略应用实例数量。
  7. 用 authenticate() 通过来证明所有表和查询都正确。
  8. 在日志中输出完整连接配置或登录请求体。

12. 动手练习

  1. 从项目根目录执行管理员创建脚本,指出 SELECT 1+1 与 INSERT 分别来自哪里。
  2. 写出 admin_user.id 从 MySQL 到 JWT sub 的完整类型链路。
  3. 用 DBeaver 查看 admin_user 的真实字符集和排序规则,不根据 Model 猜测。
  4. 为开发环境设计一个 pool 配置,并说明每个数字的依据。
  5. 分别写出 CLI 和 Web 服务关闭连接池的时机。

13. 复盘问题

  1. Sequelize 与 mysql2 分别负责什么?
  2. 为什么一个进程通常只需要一个 Sequelize 实例?
  3. authenticate() 能证明什么,不能证明什么?
  4. 为什么 String(admin.id) 无法修复已丢失的精度?
  5. db.close() 为什么适合 CLI 的 finally,却不适合请求处理函数?
  6. process.cwd() 为什么会影响 dotenv?

下一章将从 SQL DDL 开始,完整推导出 TypeScript Sequelize Model。