Node系列 · ORM:Sequelize 模型

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
相关推荐
whcyhhh1 小时前
头歌实践教学平台:数据科学与大数据技术导论(五)
大数据·数据库·python
大鹏说大话1 小时前
用 Python 与 DeviceAtlas 构建自动化设备数据库
数据库·python·自动化
肠畔码农1 小时前
深度解密 Redis 核心引擎:从 MULTI/EXEC 事务机制到 EVAL 脚本的原子性演进
数据库·redis·junit
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 6 章 Spring Boot 最佳实践 阅读笔记 11
spring boot·笔记·后端
对象存储与RustFS1 小时前
用 rclone 把现有 S3/MinIO 数据同步到 RustFS
后端·rust·开源
lailai04102 小时前
定制化企业网盘与标准化方案
数据库
PHP实战开发录2 小时前
AI接口结构化输出解析异常排查记录
数据库·安全·ai·php·开发
魔兽大山哥2 小时前
【NL2SQL 实战 05】sqlglot 这把刀:把 SQL 当结构处理,安全校验才不靠碰运气
后端
有来技术2 小时前
youlai-boot 实战:MinIO 停止维护,Docker 迁移 RustFS 完整记录
java·后端·docker