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 默认不触发 validate 和 hooks——这是性能优化。要校验显式开 validate: true。
1.3 create 的副作用
create 会触发这些钩子(按顺序):
beforeValidateafterValidatebeforeCreatebeforeSaveINSERTSQL 执行afterCreateafterSave
二、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.UPDATE | SELECT ... FOR UPDATE(排他锁) |
t.LOCK.SHARE | SELECT ... LOCK IN SHARE MODE(共享锁) |
WARNING
悲观锁只在事务内有效。脱离事务的 lock 选项会被 Sequelize 忽略。
九、最佳实践
| 场景 | 推荐 |
|---|---|
| 创建 | Model.create() 单条;批量用 bulkCreate |
| 查询 | 必须有 where;分页用 findAndCountAll |
| 软删除 | 模型开 paranoid: true;restore() 恢复 |
| 批量操作 | 默认不开 individualHooks,性能优先 |
| 事务 | 多步关联写操作用 sequelize.transaction 包裹 |
| 高并发扣减 | lock: t.LOCK.UPDATE 行级锁 |
| 复杂查询 | 不被 ORM 束缚,sequelize.query('SELECT ...') 跑 raw SQL |
十、小结
create触发完整 hooks(beforeCreate/afterCreate等)bulkCreate/Model.update/Model.destroy默认不触发 hooks,性能高findAndCountAll一次完成"分页 + 计数",是分页接口标配- 复杂条件用
Op(Op.gt/Op.like/Op.in/Op.or等) - 软删除开
paranoid: true;destroy()写deletedAt,查询自动过滤 - 事务 + 行级锁(
lock: t.LOCK.UPDATE)解决高并发写冲突
