对应官方文档:Model Basics
完成本章后,你应该能够:
Model.init() 完成映射;sync() 大致生成的 SQL,并知道为什么生产环境通常不用它改表;本文包含多个带 await 的片段。它们默认位于 async 函数、Express 异步路由或服务层异步函数中;当前工程编译为 CommonJS,不应直接把这些 await 当作 CommonJS 文件的顶层语句。
Sequelize 的 Model 是 JavaScript/TypeScript 类,它描述了:
NULL 等约束;可以把三者的关系理解为:
| 层次 | 示例 | 作用 |
|---|---|---|
| MySQL 表 | sakila.actor |
真正持久化数据并执行约束 |
| Sequelize 模型 | Actor 类 |
描述表映射并提供查询 API |
| 模型实例 | Actor 的一个对象 |
通常对应表中的一行 |
模型不是数据表本身。修改 TypeScript 类不会自动修改数据库;只有执行建表、迁移或 sync() 等操作,数据库结构才可能变化。
Sequelize 连接 MySQL 时,底层驱动通常是 mysql2:
import { Sequelize } from "sequelize";
export const sequelize = new Sequelize(
process.env.DB_NAME ?? "sakila",
process.env.DB_USER ?? "root",
process.env.DB_PASSWORD ?? "",
{
host: process.env.DB_HOST ?? "127.0.0.1",
port: Number(process.env.DB_PORT ?? 3306),
dialect: "mysql",
logging: console.log,
},
);
这里的职责是:
sequelize 负责连接池、事务、模型注册和 SQL 执行;mysql2 负责真正遵循 MySQL 协议与数据库通信;logging: console.log 会输出生成的 SQL,学习阶段建议保留;可以在服务启动阶段验证连接:
async function connectDatabase(): Promise<void> {
try {
await sequelize.authenticate();
console.log("MySQL 连接成功");
} catch (error: unknown) {
console.error("MySQL 连接失败", error);
process.exitCode = 1;
}
}
authenticate() 只验证能否连接,不会同步模型或修改表结构。
官方文档介绍了两种等价的基础方式:
sequelize.define();Model,再调用静态方法 init()。JavaScript 小项目使用 define() 很简洁;TypeScript 项目通常更适合“类 + init()”,因为模型实例的属性和方法更容易获得明确类型。
sequelize.define()import { DataTypes } from "sequelize";
import { sequelize } from "./sequelize";
export const Actor = sequelize.define(
"Actor",
{
actorId: {
type: DataTypes.SMALLINT.UNSIGNED,
primaryKey: true,
autoIncrement: true,
field: "actor_id",
},
firstName: {
type: DataTypes.STRING(45),
allowNull: false,
field: "first_name",
},
lastName: {
type: DataTypes.STRING(45),
allowNull: false,
field: "last_name",
},
},
{
tableName: "actor",
timestamps: false,
},
);
Actor 已经是一个模型构造器,可以调用 Actor.findAll(),查询结果则是它的实例。
Model下面直接映射 Sakila 的 actor 表:
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
} from "sequelize";
import { sequelize } from "./sequelize";
export class Actor extends Model<
InferAttributes<Actor>,
InferCreationAttributes<Actor>
> {
declare actorId: CreationOptional<number>;
declare firstName: string;
declare lastName: string;
declare lastUpdate: CreationOptional<Date>;
}
Actor.init(
{
actorId: {
type: DataTypes.SMALLINT.UNSIGNED,
primaryKey: true,
autoIncrement: true,
field: "actor_id",
},
firstName: {
type: DataTypes.STRING(45),
allowNull: false,
field: "first_name",
},
lastName: {
type: DataTypes.STRING(45),
allowNull: false,
field: "last_name",
},
lastUpdate: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: "last_update",
},
},
{
sequelize,
tableName: "actor",
timestamps: false,
},
);
几个 TypeScript 类型值得单独理解:
InferAttributes<Actor>:从类中声明的属性推导“查询出来的完整实例”类型;InferCreationAttributes<Actor>:推导“创建记录时允许传入的属性”类型;CreationOptional<number>:该属性在完整实例上存在,但创建时可以省略,例如数据库自增主键;declare:只向 TypeScript 描述实例属性,不会生成覆盖 Sequelize getter/setter 的运行时代码。不要把 actorId 写成普通可选属性 actorId?: number 来替代 CreationOptional。二者表达的含义不同:记录创建完成后,自增主键应当存在,而不是永久“可能不存在”。
type 是运行时映射,不是 TypeScript 类型title: {
type: DataTypes.STRING(128),
allowNull: false,
}
title: string 只负责静态类型检查;DataTypes.STRING(128) 告诉 Sequelize 如何生成和读取 SQL;VARCHAR(128) 才是真正存储数据的列类型。三层应保持一致,但它们不会自动互相取代。
| Sequelize | 常见 MySQL 类型 | TypeScript 中常用类型 | 注意事项 |
|---|---|---|---|
DataTypes.STRING(100) |
VARCHAR(100) |
string |
长度是数据库约束的一部分 |
DataTypes.TEXT |
TEXT |
string |
不适合建立普通前缀外的长索引 |
DataTypes.INTEGER |
INTEGER |
number |
可配合 .UNSIGNED |
DataTypes.BIGINT |
BIGINT |
常见为 string |
超过 JS 安全整数范围时不能用 number 精确表示 |
DataTypes.DECIMAL(10, 2) |
DECIMAL(10,2) |
常见为 string |
金额不要随意转为浮点数计算 |
DataTypes.BOOLEAN |
常见为 TINYINT(1) |
boolean |
物理类型与逻辑语义不同 |
DataTypes.DATE |
DATETIME |
Date |
需要统一时区策略 |
DataTypes.JSON |
JSON |
自定义对象类型 | 修改嵌套属性时要注意变更检测 |
MySQL 的 DECIMAL 和 BIGINT 经常被驱动以字符串形式返回,这是为了避免 JavaScript 精度丢失。模型类型必须以项目实际驱动配置和返回结果为准。
allowNull 同时包含校验和约束含义firstName: {
type: DataTypes.STRING(45),
allowNull: false,
}
创建或保存模型时,Sequelize 会阻止显式的 null;若通过模型同步建表,它还会生成 NOT NULL。但是已有表是否真的具备约束,应以 MySQL 中的 DDL 为准。
field业务代码常用 camelCase,Sakila 数据库使用 snake_case:
actorId: {
type: DataTypes.SMALLINT.UNSIGNED,
field: "actor_id",
}
于是 TypeScript 使用 actor.actorId,SQL 使用 actor_id。这比在整个应用层传播数据库命名风格更清晰。
如果整个新项目都采用这种映射,可以考虑模型选项 underscored: true。但接入已有数据库时,显式写出 field 更容易审查,也能应对不规则列名。
默认情况下,Sequelize 会根据 modelName 推导表名,并可能进行复数化。例如模型 User 默认可能映射到 Users。
接入 Sakila、World 等已有数据库时,不要依赖猜测,直接指定:
{
sequelize,
modelName: "Actor",
tableName: "actor",
timestamps: false,
}
也可以使用 freezeTableName: true 禁止表名复数化,但 tableName 的含义最明确。
timestampsSequelize 默认认为表中存在 createdAt 和 updatedAt,并在写入时维护它们:
{
timestamps: true,
}
但 Sakila 的 actor 表没有这两列,只有 last_update。因此必须使用:
{
timestamps: false,
}
然后把 last_update 当作普通字段显式映射。
对于新表,如果希望数据库列使用蛇形命名,可以这样配置:
{
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
}
工程经验:时间字段由应用还是数据库维护,应在团队中统一。不要一部分写入依赖 Sequelize,一部分依赖 MySQL ON UPDATE CURRENT_TIMESTAMP,否则更新时间的语义很难预测。
lastUpdate: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: "last_update",
}
DataTypes.NOW 是 Sequelize 在生成写入内容时使用的动态默认值。它不一定等同于数据库 DDL 中已经存在 DEFAULT CURRENT_TIMESTAMP。
如果数据还会被其他程序直接写入,应在 MySQL 层也设计合适的默认值和约束,不能只依赖 ORM。
不会。Actor.init() 只是注册模型映射。真正的建表或检查发生在执行:
await sequelize.sync();
常见形式如下:
await sequelize.sync(); // 缺表时创建
await Actor.sync(); // 只同步 Actor 模型
await sequelize.sync({ force: true }); // 删除后重建,数据会丢失
await sequelize.sync({ alter: true }); // 尝试修改现有表以匹配模型
假设模型映射到一张新表,生成 SQL 大致类似:
CREATE TABLE IF NOT EXISTS `actor` (
`actor_id` SMALLINT UNSIGNED AUTO_INCREMENT,
`first_name` VARCHAR(45) NOT NULL,
`last_name` VARCHAR(45) NOT NULL,
`last_update` DATETIME NOT NULL,
PRIMARY KEY (`actor_id`)
) ENGINE=InnoDB;
实际 SQL 受 Sequelize 版本、MySQL 版本和模型选项影响,应查看日志确认。
alter 或 forceforce: true 会删除表和数据;alter: true 自动推断结构差异,变更过程不够可控;学习环境可以用 sync() 快速验证模型;生产环境应使用有版本、可审查、可回滚评估的 migration。接入 Sakila 这类已有数据库时,通常只建立映射,根本不需要 sync()。
await Actor.drop();
await sequelize.drop();
这些 API 会真正执行 DROP TABLE。它们适合一次性测试数据库清理,不应出现在普通服务启动流程里。测试数据库也必须与开发、生产数据库使用不同的库名和权限账户。
定义模型后可以读取一行,验证字段映射是否正确:
const actor = await Actor.findByPk(1);
if (actor === null) {
console.log("演员不存在");
} else {
console.log(actor.actorId, actor.firstName, actor.lastName);
}
大致 SQL:
SELECT
`actor_id` AS `actorId`,
`first_name` AS `firstName`,
`last_name` AS `lastName`,
`last_update` AS `lastUpdate`
FROM `actor` AS `Actor`
WHERE `Actor`.`actor_id` = 1;
别把 ORM 当成无需理解 SQL 的替代品。学习时同时观察 SQL,有助于尽早发现错表名、漏索引、返回列过多和关联查询膨胀等问题。
只有执行过 Actor.init() 后才能可靠调用 Actor.findAll()。实际项目常建立一个统一的模型初始化入口,先初始化所有模型,再声明关联,最后启动 HTTP 服务。
下面的类型只在编译阶段生效:
declare firstName: string;
HTTP 请求体到达运行时后仍可能包含任意值。应先使用请求校验工具验证,再传给 Sequelize;关键规则还应由 MySQL 约束兜底。
模型写 allowNull: false,不代表已有 MySQL 表一定是 NOT NULL。应使用 SHOW CREATE TABLE actor 或数据库管理工具核对真实结构。
new Sequelize() 通常在应用生命周期中创建一次并复用连接池。如果每次 HTTP 请求都建立新实例,会快速消耗数据库连接。
Sequelize 面向 MySQL 等关系数据库,不负责 Redis。以后引入 Redis 时,可把它用于缓存、会话或临时状态;MySQL 模型仍是持久数据结构和事实来源。
timestamps?field 映射到真实列?primaryKey、autoIncrement 和 CreationOptional?DECIMAL、BIGINT、日期时区是否按照实际驱动行为设计类型?sync({ force/alter })?Model.init() 为 Sakila 的 language 表定义 Language 模型,显式映射 language_id、name 和 last_update。category 表定义模型,解释为什么要设置 timestamps: false,并写出 findByPk(1) 预计生成的 SQL。city 表定义 TypeScript 模型。注意 ID 是自增主键,而 CountryCode 的大小写和命名风格与 TypeScript 不同。country 表选取 Code、Name、Continent、Population、LifeExpectancy 五列建立模型,判断每列适合的 Sequelize 和 TypeScript 类型。actor.last_update 的 MySQL 默认值与本章模型定义,记录不一致之处,但不要执行 sync({ alter: true })。study_notes,在模型中启用 createdAt 和 updatedAt,观察 sequelize.sync() 生成的 SQL。Actor 的 tableName 配置,观察 Sequelize 查询的表名及报错,再解释默认复数化对已有数据库的影响。country.Population 暂时错误映射为 DataTypes.STRING,比较模型声明、生成 SQL和真实 MySQL 类型之间的冲突。