00 学习路线与实践环境
1. 本章目标
完成本章后,你应该能够:
- 说清 Sequelize、mysql2 和 MySQL 的分工;
- 找到当前请求从 Controller 到数据库的完整调用路径;
- 安全地连接练习数据库并观察生成的 SQL;
- 区分连接测试、业务查询、连接池和进程退出;
- 建立“先预测 SQL,再运行 ORM”的练习方式。
本章不追求复杂功能。先把观察环境准备好,后面每个 CRUD、事务和关联示例才不会变成盲目复制。
2. Sequelize 位于哪一层
当前项目的管理员登录大致经过:
POST /api/auth/login
↓
routes/auth.ts
↓
controllers/auth.controller.ts
↓
services/auth.service.ts
↓
models/admin_user.ts
↓
Sequelize
↓
mysql2
↓
MySQL Server
各部分职责如下:
| 部分 | 主要职责 |
|---|---|
| Controller | 读取 HTTP 请求,调用 Service,组织响应 |
| Service | 执行业务规则和事务编排 |
| Sequelize Model | 描述表映射,提供查询和写入接口 |
| Sequelize | 生成 SQL、映射结果、管理事务和连接池 |
| mysql2 | 实现 MySQL 网络协议,把 SQL 发给服务器 |
| MySQL | 真正保存数据、执行约束、索引、锁和事务 |
Sequelize 不会代替 MySQL 执行查询。下面的代码:
const admin = await AdminUser.findOne({
where: { username: "admin" },
});
最终仍然会变成类似的 SQL:
SELECT
`id`,
`username`,
`display_name`,
`email`,
`account_status`,
`token_version`,
`failed_login_count`,
`locked_until`,
`last_login_at`,
`password_changed_at`,
`created_at`,
`updated_at`
FROM `admin_user`
WHERE `username` = 'admin'
LIMIT 1;
实际字段会受 Scope 和 Sequelize 版本影响,因此不要背生成文本,要理解查询语义并查看真实日志。
3. 三层真相
开发中经常把下面三件事混为一谈:
3.1 Model 声明
declare email: string | null;
这是 TypeScript 对程序员的承诺,只参与类型检查。
3.2 Sequelize 映射
email: {
type: DataTypes.STRING(254),
allowNull: true,
}
这是 Sequelize 在运行时使用的表映射和部分校验信息。
3.3 MySQL 真实结构
email VARCHAR(254) NULL
这才是数据库真正执行写入、约束和查询时依赖的结构。
修改其中一层不会神奇地自动修改另外两层。后续教程会反复检查这三层是否一致。
4. 当前连接配置
当前 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",
},
});
它创建的是一个 Sequelize 实例。这个实例内部管理连接池,不表示在模块加载的一瞬间就为每次请求永久占用一条独立连接。
环境变量至少需要:
DB_NAME=sakila
DB_HOST=127.0.0.1
DB_USER=你的本地练习用户
DB_PASSWORD=你的本地练习密码
不要把包含真实凭据的 .env 提交到 Git。
5. authenticate() 是什么
执行:
await db.authenticate();
Sequelize 会从连接池取得连接,并执行一个很小的测试查询。MySQL 方言常见日志是:
Executing (default): SELECT 1+1 AS result
这只能证明此时此刻:
- 主机和端口可访问;
- 用户名与密码能够认证;
- 用户能够连接目标数据库;
- 数据库服务器能够执行基础查询。
它不能证明:
admin_user表一定存在;- Model 与真实表结构一致;
- 当前用户拥有全部 DDL/DML 权限;
- 后续连接永远不会中断。
6. SQL 日志应该怎样使用
Sequelize 默认可以输出 SQL。学习阶段建议保留日志,因为它能建立 ORM 与 SQL 的对应关系。
可以显式提供日志函数:
const db = new Sequelize({
// 其他配置
logging: (sql) => {
console.log({ event: "sequelize_sql", sql });
},
});
但生产环境需要谨慎。根据查询方式和库配置,日志可能包含:
- 用户输入;
- 邮箱等个人信息;
- SQL 参数;
- 表结构和内部字段。
不要为了调试改成记录完整 Model、req.body、密码或 JWT。
学习时可采用以下步骤:
- 先在纸上写出预期 SQL 的
SELECT、WHERE和LIMIT; - 运行 Sequelize 代码;
- 比较日志中的真实 SQL;
- 把 SQL 复制到本地练习库的 DBeaver 或 MySQL CLI;
- 使用
EXPLAIN检查查询计划; - 解释差异来自 Scope、默认字段、关联还是分页实现。
7. 连接池与 close()
Web 服务会长期运行。正常情况下,同一个进程共享同一个 Sequelize 实例和连接池:
请求 A ─┐
请求 B ─┼→ Sequelize 连接池 → 若干 MySQL 连接
请求 C ─┘
因此不能在每次 HTTP 请求结束时调用:
await db.close();
否则后续请求无法继续使用这个实例建立查询。
一次性 CLI 脚本不同。脚本执行完就应该释放自己的连接池:
void main()
.catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
})
.finally(async () => {
await db.close();
});
如果 Web 服务和 CLI 是两个独立 Node.js 进程,它们各自加载模块、创建各自的 Sequelize 实例。CLI 关闭自己的连接池,不会隔空关闭另一个进程中的连接池。
8. 建立安全的练习数据库
建议为教程创建单独数据库,而不是直接对重要数据执行实验:
CREATE DATABASE sakila_practice
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
然后使用权限受限的练习用户。不要使用生产管理员账号练习 DROP TABLE、ALTER TABLE 或锁等待。
每次执行危险 SQL 前先确认:
SELECT DATABASE();
SELECT CURRENT_USER();
执行数据修改前先把同一个条件用于查询:
SELECT id, username
FROM admin_user
WHERE id = 1;
UPDATE admin_user
SET display_name = 'Administrator'
WHERE id = 1;
9. 第一个观察实验
新建临时学习脚本时,可以完成下面几步。脚本不是本章要求直接加入当前业务代码的修改。
import "dotenv/config";
import db from "../db";
import AdminUser from "../models/admin_user";
async function main(): Promise<void> {
await db.authenticate();
const count = await AdminUser.count();
console.log({ event: "admin_count", count });
const admin = await AdminUser.findOne({
attributes: ["id", "username", "account_status"],
order: [["id", "ASC"]],
});
console.log(admin?.toJSON() ?? null);
}
void main()
.catch((error: unknown) => {
console.error({ event: "sequelize_practice_failed", error });
process.exitCode = 1;
})
.finally(async () => {
await db.close();
});
运行前先预测:
- 总共会看到几条 SQL;
count()会生成怎样的聚合;findOne()为什么会包含LIMIT 1;attributes如何影响SELECT字段;- 如果表为空,
admin的值是什么。
不要在日志里输出通过 withPassword Scope 查询到的完整管理员对象,否则可能把 password_hash 写入终端记录。
10. 常见误区
误区一:authenticate 成功等于全部功能正常
authenticate() 不检查全部表、字段、索引和权限。真正的业务查询仍可能失败。
误区二:ORM 可以让我不学 SQL
索引、事务、锁、关联和性能问题最终都发生在 MySQL 中。不会 SQL 就无法判断 ORM 查询是否正确、高效。
误区三:TS 没报错,运行时就不会错
TypeScript 不会连接数据库检查真实表结构,也不会验证环境变量和数据库响应。静态类型只是开发期保护。
误区四:每次操作都创建 Sequelize
这会产生多个连接池,浪费连接并增加关闭管理难度。当前项目使用 db.ts 导出的单例,适合单进程服务。
误区五:close 会关闭整个 MySQL
db.close() 只关闭当前 Sequelize 实例管理的连接,不会关闭 MySQL Server,也不会关闭其他进程的连接池。
11. 动手练习
- 运行
db.authenticate(),找到对应 SQL。 - 故意把本地练习环境的数据库名改错,观察错误类型;恢复配置后不要提交
.env。 - 分别执行
AdminUser.count()和AdminUser.findAll(),比较 SQL 与返回类型。 - 给
findAll()加上attributes、order和limit,观察 SQL 变化。 - 从根目录和
scripts/目录分别运行一个 dotenv 脚本,解释process.cwd()对.env查找的影响。 - 开启 Web 服务后,在另一个 CLI 进程中连接再关闭 Sequelize,验证 Web 服务仍可查询。
12. 复盘问题
- Sequelize 和 mysql2 分别负责什么?
authenticate()为什么会执行SELECT 1+1 AS result?- 为什么 Model 类型正确不能证明数据库表结构正确?
- 为什么 Web 请求结束时不应调用
db.close()? - 为什么 CLI 脚本通常需要在
finally中关闭连接池? - 观察 Sequelize SQL 日志时,需要防止哪些敏感信息泄露?
- 一条 Sequelize 查询出现性能问题时,你会按什么顺序排查?