Node系列 · ORM:Sequelize 模型
模型(Model)是 Sequelize 的核心抽象------一张数据库表对应一个 Model 类。本章讲清楚模型定义的两种风格、字段约束、模型同步(sync vs migrations)、以及模型生命周期钩子(hooks)。
一、两种定义风格
1.1 一次性 sequelize.define
最直接:在一个文件里定义所有模型。适合小型项目或 demo:
javascript:sequelize-define.js
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize(/* config */);
const User = sequelize.define('User', {
name: { type: DataTypes.STRING(50), allowNull: false },
email: { type: DataTypes.STRING(100), allowNull: false, unique: true },
});
const Post = sequelize.define('Post', {
title: { type: DataTypes.STRING(200), allowNull: false },
content: { type: DataTypes.TEXT },
});
module.exports = { User, Post };
1.2 单独 model 文件
大型项目推荐:每个模型单独文件,统一入口注册:
text:model-proj/
models/
├── index.js # 注册入口
├── user.js # User 模型
├── post.js # Post 模型
└── comment.js # Comment 模型
javascript:models/user.js
const { DataTypes, Model } = require('sequelize');
class User extends Model {
static init(sequelize) {
return super.init({
name: { type: DataTypes.STRING(50), allowNull: false },
email: { type: DataTypes.STRING(100), allowNull: false, unique: true },
}, {
sequelize,
modelName: 'User',
tableName: 'users', // 显式表名
timestamps: true, // 自动加 createdAt / updatedAt
underscored: true, // 用 snake_case(created_at 而非 createdAt)
});
}
}
module.exports = User;
javascript:models/index.js
const sequelize = require('../config/database');
const User = require('./user');
const Post = require('./post');
User.init(sequelize);
Post.init(sequelize);
// 后续章节讲关联
// User.hasMany(Post);
// Post.belongsTo(User);
module.exports = { sequelize, User, Post };
二、字段约束
完整的字段定义可以包含类型、约束、默认值、校验等:
javascript:column-options.js
{
type: DataTypes.STRING(50),
allowNull: false, // NOT NULL
unique: true, // UNIQUE
primaryKey: true, // PRIMARY KEY
autoIncrement: true, // AUTO_INCREMENT
defaultValue: 'guest', // 默认值
validate: { // 值校验
notEmpty: true, // 不能是空字符串
len: [2, 50], // 长度 2-50
isEmail: true, // 必须邮箱格式
},
comment: '用户姓名', // 注释(生成 SQL 时会变成 COMMENT)
}
2.1 常用校验器
| 校验器 | 作用 |
|---|---|
notEmpty |
非空字符串 |
notNull |
非 null(仅校验已定义的字段) |
len: [min, max] |
字符串长度 |
min / max |
数值范围 |
isEmail |
邮箱格式 |
isUrl |
URL 格式 |
isInt |
整数 |
isIn: [...] |
在指定集合中 |
isBefore / isAfter |
日期范围 |
javascript:validate-demo.js
const User = sequelize.define('User', {
age: {
type: DataTypes.INTEGER,
validate: {
min: 0,
max: 150,
},
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'banned'),
defaultValue: 'active',
},
});
三、模型同步:sync vs migrations
3.1 sync(适合开发和小项目)
javascript:sync-demo.js
// 同步所有模型到数据库
await sequelize.sync();
// force: true 会先 DROP 再 CREATE(危险!)
await sequelize.sync({ force: true });
// alter: true 会按模型定义调整表结构(保留数据)
await sequelize.sync({ alter: true });
// 只同步单个模型
await User.sync({ alter: true });
| 选项 | 行为 |
|---|---|
| 不传参 | 表不存在则创建;存在则不动 |
force: true |
表存在则 DROP 再 CREATE(丢数据) |
alter: true |
按模型定义 ALTER TABLE(保留数据) |
::: warning
生产环境不要用 sync。它无法处理:
- 字段重命名(会被识别为删一个字段 + 加一个字段)
- 字段类型变更(可能数据丢失)
- 多实例部署时多个进程同时 sync 冲突
生产用 migrations 。
:::
3.2 migrations(生产环境标准做法)
bash
# 初始化 migrations 目录
npx sequelize-cli init:migrations
# 生成迁移文件
npx sequelize-cli migration:generate --name add-user-avatar
# 执行迁移
npx sequelize-cli db:migrate
# 回滚最近一次
npx sequelize-cli db:migrate:undo
迁移文件示例:
javascript:20240815120000-add-user-avatar.js
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'avatar', {
type: Sequelize.STRING(200),
allowNull: true,
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('users', 'avatar');
},
};
up 是正向迁移,down 是回滚。queryInterface 是 Sequelize 提供的底层 SQL 接口,支持加字段、改类型、建索引等所有 schema 操作。
四、模型生命周期钩子(Hooks)
Hook 在 CRUD 关键节点触发------非常适合做数据清洗、加密、关联写入:
javascript:hooks-demo.js
const User = sequelize.define('User', {
name: DataTypes.STRING,
email: { type: DataTypes.STRING, validate: { isEmail: true } },
password: DataTypes.STRING,
}, {
hooks: {
// 创建前:自动加密密码
beforeCreate: async (user) => {
if (user.password) {
user.password = await hashPassword(user.password);
}
},
// 更新前:规范化邮箱
beforeUpdate: async (user) => {
if (user.changed('email')) {
user.email = user.email.toLowerCase();
}
},
// 创建后:发欢迎邮件
afterCreate: async (user) => {
await sendWelcomeEmail(user.email);
},
},
});
4.1 Hook 时机
| 时机 | 触发点 |
|---|---|
beforeValidate |
校验前 |
afterValidate |
校验后 |
beforeCreate / afterCreate |
创建单条前后 |
beforeUpdate / afterUpdate |
更新单条前后 |
beforeDestroy / afterDestroy |
删除单条前后 |
beforeSave / afterSave |
创建或更新前后 |
beforeBulkCreate 等 |
批量操作 |
4.2 全局钩子
对所有模型生效:
javascript:global-hooks.js
const sequelize = new Sequelize(/* config */, {
define: {
hooks: {
beforeCreate: async (instance) => {
// 所有模型创建前都会触发
},
},
},
});
五、模型选项
javascript:model-options.js
{
sequelize,
modelName: 'User', // 模型名
tableName: 'users', // 数据库表名(默认模型名复数)
timestamps: true, // 自动加 createdAt / updatedAt
createdAt: 'created_at', // 自定义时间字段名
updatedAt: 'updated_at',
paranoid: true, // 软删除(加 deletedAt 字段)
underscored: true, // 字段 snake_case
freezeTableName: true, // 禁用表名复数化
indexes: [ // 索引定义
{ fields: ['email'], unique: true },
{ fields: ['created_at'] },
],
}
::: tip
paranoid: true 是软删除的关键 。开启后 destroy() 不会真删,而是设置 deletedAt,查询自动过滤。这是推荐的"假删除"实现方式。
:::
六、模型关联(Relationships)
模型间的关系是 ORM 的核心抽象:
javascript:associations.js
// 一对多:User 有多个 Post
User.hasMany(Post, { foreignKey: 'userId' });
Post.belongsTo(User, { foreignKey: 'userId' });
// 多对多:Post 有多个 Tag,Tag 属于多个 Post
Post.belongsToMany(Tag, { through: 'PostTags' });
Tag.belongsToMany(Post, { through: 'PostTags' });
// 一对一:User 有一个 Profile
User.hasOne(Profile, { foreignKey: 'userId' });
Profile.belongsTo(User, { foreignKey: 'userId' });
关联后可以通过 include 做 JOIN 查询(细节见 数据查询 章节)。
七、最佳实践
| 场景 | 推荐 |
|---|---|
| 模型定义 | 大项目用单独文件 + init();小项目用 define() |
| 字段约束 | 显式写 allowNull / defaultValue / validate |
| 时间字段 | 默认 timestamps: true + underscored: true |
| 表同步 | 开发用 sync({ alter: true });生产用 migrations |
| 软删除 | paranoid: true + deletedAt |
| Hooks | 在 Hook 里做数据清洗、加密、关联写入 |
| 命名 | 表名复数小写下划线(users);模型名单数大驼峰(User) |
八、小结
- 两种定义风格:一次性
define()(小项目)vs 单独 model 文件 +init()(大项目) - 字段约束:类型、
allowNull/unique/defaultValue/validate - 模型同步:开发用
sync({ alter: true });生产必须用 migrations - Hooks 在 CRUD 关键节点触发------做数据清洗、加密、关联写入
paranoid: true实现软删除(destroy()不真删)- 模型关联:
hasMany/belongsTo/belongsToMany/hasOne