Skip to content

Node系列 · ORM:模型增删改

模型 CRUD 是 Sequelize 最日常的操作:create / findAll / update / destroy。它们把 SQL 翻译成 JS 方法,但要写出高效代码还得理解底层行为、查询运算符、批量操作和事务。

一、Create 新增

1.1 单条新增

javascript
const user = await User.create({
  name: 'Alice',
  email: 'alice@example.com',
  age: 25,
});

console.log(user.id); // 自增主键,create 后立即可用
console.log(user.createdAt); // 自动填充

1.2 批量新增

javascript
await User.bulkCreate([
  { name: 'Bob', email: 'bob@example.com' },
  { name: 'Carol', email: 'carol@example.com' },
  { name: 'Dave', email: 'dave@example.com' },
], {
  validate: true,    // 触发字段 validate
  ignoreDuplicates: true,  // 唯一键冲突时跳过(MySQL INSERT IGNORE)
});

bulkCreate 默认不触发 validatehooks——这是性能优化。要校验显式开 validate: true

1.3 create 的副作用

create 会触发这些钩子(按顺序):

  1. beforeValidate
  2. afterValidate
  3. beforeCreate
  4. beforeSave
  5. INSERT SQL 执行
  6. afterCreate
  7. afterSave

二、Read 查询

2.1 findAll 查多条

javascript
const users = await User.findAll({
  where: { isActive: true },
  attributes: ['id', 'name', 'email'],  // 指定字段,避免 SELECT *
  order: [['createdAt', 'DESC']],
  limit: 20,
  offset: 0,
});

2.2 findOne / findByPk

javascript
// 查一条(返回第一条匹配)
const user = await User.findOne({ where: { email: 'alice@example.com' } });

// 按主键查
const user = await User.findByPk(1);

2.3 findAndCountAll(分页必备)

javascript
const { count, rows } = await User.findAndCountAll({
  where: { isActive: true },
  limit: 10,
  offset: 0,
});

console.log(`共 ${count} 条,当前页 ${rows.length} 条`);

findAndCountAll 一次查询完成"取数据 + 计数",比分两次查询(findAll + count)少一次往返。

2.4 count 计数

javascript
const total = await User.count();
const activeCount = await User.count({ where: { isActive: true } });

三、where 条件运算符

简单等值用对象字面量,复杂条件用 Op

javascript
const { Op } = require('sequelize');

// 等值
where: { status: 'active' }

// 不等值
where: { status: { [Op.ne]: 'banned' } }

// 大于 / 小于
where: { age: { [Op.gt]: 18 } }            // >
where: { age: { [Op.gte]: 18 } }           // >=
where: { age: { [Op.lt]: 65 } }            // <
where: { age: { [Op.lte]: 65 } }           // <=

// 范围
where: { age: { [Op.between]: [18, 65] } }
where: { age: { [Op.notBetween]: [0, 18] } }

// 包含
where: { status: { [Op.in]: ['active', 'pending'] } }
where: { status: { [Op.notIn]: ['banned', 'deleted'] } }

// 模糊匹配(LIKE)
where: { name: { [Op.like]: '%张%' } }
where: { name: { [Op.startsWith]: '张' } }
where: { name: { [Op.endsWith]: '三' } }

// NULL
where: { deletedAt: { [Op.is]: null } }
where: { deletedAt: { [Op.not]: null } }

// OR / AND
where: {
  [Op.or]: [
    { name: { [Op.like]: '%张%' } },
    { email: { [Op.like]: '%张%' } },
  ],
}

四、Update 更新

4.1 实例级别 update

javascript
const user = await User.findByPk(1);
user.name = 'Alice2';
user.age = 26;
await user.save();

save() 会触发 beforeUpdate / afterUpdate 钩子。

4.2 批量 update(model 级别)

javascript
// 单条 SQL UPDATE
await User.update(
  { isActive: false },          // SET 部分
  { where: { lastLoginAt: { [Op.lt]: oneYearAgo } } }  // WHERE 部分
);

WARNING

批量 update 默认不触发 hooks(性能优化)。要触发需 individualHooks: true

javascript
await User.update(
  { isActive: false },
  { where: { ... }, individualHooks: true }  // 每条都触发
);

但这样性能会显著下降。生产环境建议只对单条记录走 update + hooks

4.3 upsert

javascript
// 不存在则插入,存在则更新
await User.upsert({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
});

底层是 INSERT ... ON DUPLICATE KEY UPDATE,原子操作。

五、Destroy 删除

5.1 实例删除

javascript
const user = await User.findByPk(1);
await user.destroy();

5.2 条件删除

javascript
// 单条 SQL DELETE
await User.destroy({
  where: { isActive: false, lastLoginAt: { [Op.lt]: oneYearAgo } },
});

5.3 软删除(paranoid 模型)

开启 paranoid: true 后,destroy() 不真删而是写 deletedAt

javascript
// "删除"
await user.destroy();  // 实际 UPDATE users SET deletedAt = NOW()

// 查询自动过滤已删除的
const users = await User.findAll();  // 不含 deletedAt 非空的

// 查全部(含已删除)
const all = await User.findAll({ paranoid: false });

// 恢复软删除的记录
await user.restore();

TIP

生产环境几乎都用 paranoid: true。保留数据用于审计和恢复,符合合规要求。

六、批量操作注意事项

操作默认触发 hooks性能何时用
bulkCreate大批量导入
Model.update批量状态变更
Model.destroy批量删除
Model.bulkCreate(..., { individualHooks: true })慢(每条都触发)需要校验的批量
Model.update(..., { individualHooks: true })需要逐条钩子的批量

七、事务

javascript
const { sequelize } = require('./models');

const t = await sequelize.transaction();
try {
  const user = await User.create({ name: 'Alice', email: 'alice@x.com' }, { transaction: t });
  await Profile.create({ userId: user.id, bio: '...' }, { transaction: t });

  await t.commit();
} catch (err) {
  await t.rollback();
  throw err;
}

// 简写:自动 commit / rollback
const result = await sequelize.transaction(async (t) => {
  const user = await User.create({ name: 'Bob' }, { transaction: t });
  await Profile.create({ userId: user.id }, { transaction: t });
  return user;
});

八、悲观锁

高并发场景下用 SELECT ... FOR UPDATE 防止并发写冲突:

javascript
const t = await sequelize.transaction();

const user = await User.findByPk(1, {
  transaction: t,
  lock: t.LOCK.UPDATE,           // SELECT ... FOR UPDATE
});

user.balance -= 100;
await user.save({ transaction: t });

await t.commit();
锁类型SQL
t.LOCK.UPDATESELECT ... FOR UPDATE(排他锁)
t.LOCK.SHARESELECT ... LOCK IN SHARE MODE(共享锁)

WARNING

悲观锁只在事务内有效。脱离事务的 lock 选项会被 Sequelize 忽略。

九、最佳实践

场景推荐
创建Model.create() 单条;批量用 bulkCreate
查询必须有 where;分页用 findAndCountAll
软删除模型开 paranoid: truerestore() 恢复
批量操作默认不开 individualHooks,性能优先
事务多步关联写操作用 sequelize.transaction 包裹
高并发扣减lock: t.LOCK.UPDATE 行级锁
复杂查询不被 ORM 束缚,sequelize.query('SELECT ...') 跑 raw SQL

十、小结

  • create 触发完整 hooks(beforeCreate / afterCreate 等)
  • bulkCreate / Model.update / Model.destroy 默认不触发 hooks,性能高
  • findAndCountAll 一次完成"分页 + 计数",是分页接口标配
  • 复杂条件用 OpOp.gt / Op.like / Op.in / Op.or 等)
  • 软删除开 paranoid: truedestroy()deletedAt,查询自动过滤
  • 事务 + 行级锁(lock: t.LOCK.UPDATE)解决高并发写冲突