Sequelize迁移事务失效:错误发生后未回滚已执行操作
问题分析与解决
问题重现
使用Sequelize编写数据库迁移脚本,在事务中执行两个addColumn操作,故意写错第二个表名触发错误。期望事务回滚时移除已创建的petName字段,但实际字段依然存在,即使去掉transaction.commit()也是如此:
module.exports = { async up(queryInterface, Sequelize) { const transaction = await queryInterface.sequelize.transaction(); try { await queryInterface.addColumn( 'Person', 'petName', { type: Sequelize.DataTypes.STRING, }, { transaction } ); await queryInterface.addColumn( 'Personasfasfasfasf', 'petAge', { type: Sequelize.DataTypes.INTEGER, }, {transaction} ); await transaction.commit(); } catch (err) { await transaction.rollback(); throw err; } }, async down(queryInterface, Sequelize) { const transaction = await queryInterface.sequelize.transaction(); try { await queryInterface.removeColumn('Person', 'petName', { transaction }); await queryInterface.removeColumn('Person', 'petAge', { transaction }); await transaction.commit(); } catch (err) { await transaction.rollback(); throw err; } } };
数据库为MySQL,Person表存在,第二个表名错误,执行迁移时进入catch块,但Person.petName字段未被回滚。
核心原因
这不是Sequelize的问题,是MySQL本身的特性限制:
- MySQL中所有DDL语句(比如
ADD COLUMN、CREATE TABLE这类结构变更语句)执行时会触发隐式提交——只要执行DDL,当前事务会被自动提交,不管你有没有显式调用commit()。 - 所以第一个
addColumn执行完成后,事务已经被MySQL自动提交了,后续的错误回滚根本无法撤销这个已经持久化的操作。 - 哪怕你去掉
transaction.commit(),DDL的隐式提交也会让字段被永久创建。
解决方案
因为MySQL不支持DDL事务回滚,要实现类似原子性的效果,只能换思路:
1. 先校验依赖对象是否存在
在执行第二个addColumn之前,先检查目标表是否存在,避免触发错误:
async up(queryInterface, Sequelize) { const transaction = await queryInterface.sequelize.transaction(); try { // 先检查表是否存在 const tableExists = await queryInterface.sequelize.query( "SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'Personasfasfasfasf'", { transaction } ); if (!tableExists[0].length) { throw new Error('目标表不存在'); } // 再执行DDL操作 await queryInterface.addColumn('Person', 'petName', { type: Sequelize.STRING }, { transaction }); await queryInterface.addColumn('Personasfasfasfasf', 'petAge', { type: Sequelize.INTEGER }, { transaction }); await transaction.commit(); } catch (err) { await transaction.rollback(); throw err; } }
2. 手动回滚已执行的DDL
如果已经执行了部分DDL才触发错误,手动执行反向操作撤销:
async up(queryInterface, Sequelize) { const transaction = await queryInterface.sequelize.transaction(); let petNameAdded = false; try { await queryInterface.addColumn('Person', 'petName', { type: Sequelize.STRING }, { transaction }); petNameAdded = true; await queryInterface.addColumn('Personasfasfasfasf', 'petAge', { type: Sequelize.INTEGER }, { transaction }); await transaction.commit(); } catch (err) { await transaction.rollback(); // 手动移除已添加的字段 if (petNameAdded) { await queryInterface.removeColumn('Person', 'petName'); } throw err; } }
3. 更换支持DDL事务的数据库
如果业务对DDL原子性要求极高,可以考虑切换到PostgreSQL——它支持在事务中执行DDL,出错时可以完整回滚所有操作。
内容的提问来源于stack exchange,提问作者Kshitij
相关产品推荐
相关产品推荐

