Sequelize v6 教程 01:Model Basics

对应官方文档:Model Basics

1. 本章目标

完成本章后,你应该能够:

本文包含多个带 await 的片段。它们默认位于 async 函数、Express 异步路由或服务层异步函数中;当前工程编译为 CommonJS,不应直接把这些 await 当作 CommonJS 文件的顶层语句。

2. Model 到底是什么

Sequelize 的 Model 是 JavaScript/TypeScript 类,它描述了:

可以把三者的关系理解为:

层次 示例 作用
MySQL 表 sakila.actor 真正持久化数据并执行约束
Sequelize 模型 Actor 描述表映射并提供查询 API
模型实例 Actor 的一个对象 通常对应表中的一行

模型不是数据表本身。修改 TypeScript 类不会自动修改数据库;只有执行建表、迁移或 sync() 等操作,数据库结构才可能变化。

3. 建立 Sequelize 连接

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,
  },
);

这里的职责是:

可以在服务启动阶段验证连接:

async function connectDatabase(): Promise<void> {
  try {
    await sequelize.authenticate();
    console.log("MySQL 连接成功");
  } catch (error: unknown) {
    console.error("MySQL 连接失败", error);
    process.exitCode = 1;
  }
}

authenticate() 只验证能否连接,不会同步模型或修改表结构。

4. 两种模型定义方式

官方文档介绍了两种等价的基础方式:

  1. sequelize.define()
  2. 继承 Model,再调用静态方法 init()

JavaScript 小项目使用 define() 很简洁;TypeScript 项目通常更适合“类 + init()”,因为模型实例的属性和方法更容易获得明确类型。

4.1 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(),查询结果则是它的实例。

4.2 TypeScript 推荐:继承 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 类型值得单独理解:

不要把 actorId 写成普通可选属性 actorId?: number 来替代 CreationOptional。二者表达的含义不同:记录创建完成后,自增主键应当存在,而不是永久“可能不存在”。

5. 模型属性与 MySQL 列的映射

5.1 type 是运行时映射,不是 TypeScript 类型

title: {
  type: DataTypes.STRING(128),
  allowNull: false,
}

三层应保持一致,但它们不会自动互相取代。

5.2 常用数据类型

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 的 DECIMALBIGINT 经常被驱动以字符串形式返回,这是为了避免 JavaScript 精度丢失。模型类型必须以项目实际驱动配置和返回结果为准。

5.3 allowNull 同时包含校验和约束含义

firstName: {
  type: DataTypes.STRING(45),
  allowNull: false,
}

创建或保存模型时,Sequelize 会阻止显式的 null;若通过模型同步建表,它还会生成 NOT NULL。但是已有表是否真的具备约束,应以 MySQL 中的 DDL 为准。

5.4 属性名和列名不同:使用 field

业务代码常用 camelCase,Sakila 数据库使用 snake_case:

actorId: {
  type: DataTypes.SMALLINT.UNSIGNED,
  field: "actor_id",
}

于是 TypeScript 使用 actor.actorId,SQL 使用 actor_id。这比在整个应用层传播数据库命名风格更清晰。

如果整个新项目都采用这种映射,可以考虑模型选项 underscored: true。但接入已有数据库时,显式写出 field 更容易审查,也能应对不规则列名。

6. 模型名、表名和复数化

默认情况下,Sequelize 会根据 modelName 推导表名,并可能进行复数化。例如模型 User 默认可能映射到 Users

接入 Sakila、World 等已有数据库时,不要依赖猜测,直接指定:

{
  sequelize,
  modelName: "Actor",
  tableName: "actor",
  timestamps: false,
}

也可以使用 freezeTableName: true 禁止表名复数化,但 tableName 的含义最明确。

7. 时间戳:timestamps

Sequelize 默认认为表中存在 createdAtupdatedAt,并在写入时维护它们:

{
  timestamps: true,
}

但 Sakila 的 actor 表没有这两列,只有 last_update。因此必须使用:

{
  timestamps: false,
}

然后把 last_update 当作普通字段显式映射。

对于新表,如果希望数据库列使用蛇形命名,可以这样配置:

{
  timestamps: true,
  createdAt: "created_at",
  updatedAt: "updated_at",
}

工程经验:时间字段由应用还是数据库维护,应在团队中统一。不要一部分写入依赖 Sequelize,一部分依赖 MySQL ON UPDATE CURRENT_TIMESTAMP,否则更新时间的语义很难预测。

8. 默认值

lastUpdate: {
  type: DataTypes.DATE,
  allowNull: false,
  defaultValue: DataTypes.NOW,
  field: "last_update",
}

DataTypes.NOW 是 Sequelize 在生成写入内容时使用的动态默认值。它不一定等同于数据库 DDL 中已经存在 DEFAULT CURRENT_TIMESTAMP

如果数据还会被其他程序直接写入,应在 MySQL 层也设计合适的默认值和约束,不能只依赖 ORM。

9. 创建模型后会立即建表吗

不会。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 版本和模型选项影响,应查看日志确认。

9.1 为什么生产环境不建议依赖 alterforce

学习环境可以用 sync() 快速验证模型;生产环境应使用有版本、可审查、可回滚评估的 migration。接入 Sakila 这类已有数据库时,通常只建立映射,根本不需要 sync()

10. 删除表与测试环境

await Actor.drop();
await sequelize.drop();

这些 API 会真正执行 DROP TABLE。它们适合一次性测试数据库清理,不应出现在普通服务启动流程里。测试数据库也必须与开发、生产数据库使用不同的库名和权限账户。

11. 一个最小查询验证

定义模型后可以读取一行,验证字段映射是否正确:

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,有助于尽早发现错表名、漏索引、返回列过多和关联查询膨胀等问题。

12. 常见错误与实践建议

12.1 模型初始化顺序错误

只有执行过 Actor.init() 后才能可靠调用 Actor.findAll()。实际项目常建立一个统一的模型初始化入口,先初始化所有模型,再声明关联,最后启动 HTTP 服务。

12.2 把 TypeScript 当成运行时校验

下面的类型只在编译阶段生效:

declare firstName: string;

HTTP 请求体到达运行时后仍可能包含任意值。应先使用请求校验工具验证,再传给 Sequelize;关键规则还应由 MySQL 约束兜底。

12.3 模型与实际 DDL 漂移

模型写 allowNull: false,不代表已有 MySQL 表一定是 NOT NULL。应使用 SHOW CREATE TABLE actor 或数据库管理工具核对真实结构。

12.4 不要在每个请求里创建 Sequelize

new Sequelize() 通常在应用生命周期中创建一次并复用连接池。如果每次 HTTP 请求都建立新实例,会快速消耗数据库连接。

12.5 Redis 的边界

Sequelize 面向 MySQL 等关系数据库,不负责 Redis。以后引入 Redis 时,可把它用于缓存、会话或临时状态;MySQL 模型仍是持久数据结构和事实来源。

13. 本章检查清单

14. 练习题(暂不提供答案)

  1. 使用类和 Model.init() 为 Sakila 的 language 表定义 Language 模型,显式映射 language_idnamelast_update
  2. 为 Sakila 的 category 表定义模型,解释为什么要设置 timestamps: false,并写出 findByPk(1) 预计生成的 SQL。
  3. 为 World 的 city 表定义 TypeScript 模型。注意 ID 是自增主键,而 CountryCode 的大小写和命名风格与 TypeScript 不同。
  4. 为 World 的 country 表选取 CodeNameContinentPopulationLifeExpectancy 五列建立模型,判断每列适合的 Sequelize 和 TypeScript 类型。
  5. 查询 Sakila 的真实 DDL,对比 actor.last_update 的 MySQL 默认值与本章模型定义,记录不一致之处,但不要执行 sync({ alter: true })
  6. 新建一个仅供练习的 MySQL 表 study_notes,在模型中启用 createdAtupdatedAt,观察 sequelize.sync() 生成的 SQL。
  7. 故意移除 ActortableName 配置,观察 Sequelize 查询的表名及报错,再解释默认复数化对已有数据库的影响。
  8. 将 World country.Population 暂时错误映射为 DataTypes.STRING,比较模型声明、生成 SQL和真实 MySQL 类型之间的冲突。

15. 官方参考