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 中:
utf8mb4:完整 Unicode;0900:基于 Unicode 9.0 的规则;ai:accent insensitive;ci:case insensitive。
因此字符串唯一性也可能不区分大小写。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,
},
});
max:最多同时保留多少连接;min:最少连接数;acquire:等待可用连接的最长时间;idle:空闲连接多久后可被释放。
这些不是越大越好。多个应用实例的连接池总和不能超过数据库承受能力。
5. 为什么整个进程只创建一个实例
错误示例:
export async function queryAdmin() {
const db = new Sequelize(/* ... */);
return AdminUser.findAll();
}
每个请求创建实例会产生多个连接池,导致:
- 连接数快速增长;
- 重复握手;
- 难以统一关闭;
- Model 可能绑定到另一个 Sequelize 实例。
Web 服务应在启动时创建一次,所有请求共享。
6. authenticate() 做了什么
await db.authenticate();
它验证当前配置能否连接和执行简单查询。日志中常见:
SELECT 1+1 AS result;
它不做以下事情:
- 不检查所有业务表都存在;
- 不执行 Migration;
- 不检查 Model 与表结构一致;
- 不代表未来网络永不失败。
因此启动成功只说明此刻基础连接可用。
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
两种执行方式加载配置的位置可能不同。推荐:
- 文档统一要求从项目根目录执行命令;
package.json提供脚本,固定入口;- 必须支持任意目录时,根据脚本文件位置解析明确的
.env路径,而不是依赖 cwd。
不要在库模块中随意改变 process.cwd()。
9. SQL 日志如何使用
Sequelize 的 logging 可以是布尔值或函数:
const db = new Sequelize({
// ...
logging: (sql) => {
console.debug({ event: "sequelize_query", sql });
},
});
开发时观察 SQL 很有价值,但要注意:
- SQL 可能包含业务数据;
- 不应记录数据库密码、JWT、明文密码或密码摘要;
- 高频查询会制造大量日志;
- 生产环境宜使用结构化日志和慢查询阈值,而不是无条件打印全部 SQL。
当前控制台出现的 Executing (default): ... 是 Sequelize 默认 SQL 日志。它一般写到进程输出,再由运行环境、PM2、Docker 或日志系统收集。
10. 开发、测试、生产配置
至少应区分:
- 开发:可查看 SQL,连接池较小;
- 测试:独立数据库,保证测试不会污染开发数据;
- 生产:Secret 由部署环境注入,日志谨慎,连接池按实例数量规划。
不要用 NODE_ENV 之外的模糊字符串隐式决定高风险行为,更不要在生产配置下运行 sync({ force: true })。
11. 初学者常见错误
- 每个 Service 创建 Sequelize 实例,造成多个连接池。
- 每次请求后调用
db.close(),下一个请求报连接管理器已关闭。 - 把
.env提交到 Git,泄露凭据。 - 认为
define.collate会自动修改现有表。 - 把 BIGINT 声明成
number,产生静默精度损失。 - 把连接池
max设置得极大,忽略应用实例数量。 - 用
authenticate()通过来证明所有表和查询都正确。 - 在日志中输出完整连接配置或登录请求体。
12. 动手练习
- 从项目根目录执行管理员创建脚本,指出
SELECT 1+1与INSERT分别来自哪里。 - 写出
admin_user.id从 MySQL 到 JWTsub的完整类型链路。 - 用 DBeaver 查看
admin_user的真实字符集和排序规则,不根据 Model 猜测。 - 为开发环境设计一个
pool配置,并说明每个数字的依据。 - 分别写出 CLI 和 Web 服务关闭连接池的时机。
13. 复盘问题
- Sequelize 与 mysql2 分别负责什么?
- 为什么一个进程通常只需要一个 Sequelize 实例?
authenticate()能证明什么,不能证明什么?- 为什么
String(admin.id)无法修复已丢失的精度? db.close()为什么适合 CLI 的finally,却不适合请求处理函数?process.cwd()为什么会影响 dotenv?
下一章将从 SQL DDL 开始,完整推导出 TypeScript Sequelize Model。