我在使用 MySQL 时遇到了 Sequelize 的奇怪行为。该脚本创建一个包含一列的表,然后将一条记录播种到该表中。然后,它使用
.forEach()
和 for (const ... of ...)
迭代 .findAll()
的结果并更新每条记录(在本例中,仅更新一条记录)。
(node.js 20.12.2 + 续集 6.37.3)
const Sequelize = require('sequelize');
let db;
if (0) {
// SQLite case:
db = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
});
} else {
// MySQL case:
db = new Sequelize({
dialect: 'mysql',
hostname: '127.0.0.1',
username: 'root',
password: process.env.DB_PASSWORD,
database: 'test',
});
}
(async () => {
const Foo = db.define('Foo', {
bar: Sequelize.DataTypes.STRING,
});
// Creating table for database:
await Foo.sync({
force: true
});
// Seed data.
await Foo.create({
bar: 'abc',
});
// forEach case:
let items = await Foo.findAll();
console.log(items[0].bar);
items.forEach(async item => {
item.bar = 'forEach';
await item.save();
});
items = await Foo.findAll();
console.log(items[0].bar);
// for-of case:
items = await Foo.findAll();
console.log(items[0].bar);
for (const item of items) {
item.bar = 'for-of';
await item.save();
};
items = await Foo.findAll();
console.log(items[0].bar);
})().finally(() => {
process.exit();
});
我设置了4个
console.log()
语句来观察数据变化。在 SQLite 中,行为符合预期:
Executing (default): DROP TABLE IF EXISTS `Foos`;
Executing (default): CREATE TABLE IF NOT EXISTS `Foos` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `bar` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
Executing (default): PRAGMA INDEX_LIST(`Foos`)
Executing (default): INSERT INTO `Foos` (`id`,`bar`,`createdAt`,`updatedAt`) VALUES (NULL,$1,$2,$3);
Executing (default): SELECT `id`, `bar`, `createdAt`, `updatedAt` FROM `Foos` AS `Foo`;
abc
Executing (default): SELECT `id`, `bar`, `createdAt`, `updatedAt` FROM `Foos` AS `Foo`;
Executing (default): UPDATE `Foos` SET `bar`=$1,`updatedAt`=$2 WHERE `id` = $3
forEach
Executing (default): SELECT `id`, `bar`, `createdAt`, `updatedAt` FROM `Foos` AS `Foo`;
forEach
Executing (default): UPDATE `Foos` SET `bar`=$1,`updatedAt`=$2 WHERE `id` = $3
Executing (default): SELECT `id`, `bar`, `createdAt`, `updatedAt` FROM `Foos` AS `Foo`;
for-of
但是,在MySQL中,
forEach
更新似乎不起作用:
Executing (default): DROP TABLE IF EXISTS `Foos`;
Executing (default): CREATE TABLE IF NOT EXISTS `Foos` (`id` INTEGER NOT NULL auto_increment , `bar` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
Executing (default): SHOW INDEX FROM `Foos`
Executing (default): INSERT INTO `Foos` (`id`,`bar`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?);
Executing (default): SELECT `id`, `bar`, `createdAt`, `updatedAt` FROM `Foos` AS `Foo`;
abc
Executing (default): SELECT `id`, `bar`, `createdAt`, `updatedAt` FROM `Foos` AS `Foo`;
Executing (default): UPDATE `Foos` SET `bar`=?,`updatedAt`=? WHERE `id` = ?
abc
Executing (default): SELECT `id`, `bar`, `createdAt`, `updatedAt` FROM `Foos` AS `Foo`;
forEach
Executing (default): UPDATE `Foos` SET `bar`=?,`updatedAt`=? WHERE `id` = ?
Executing (default): SELECT `id`, `bar`, `createdAt`, `updatedAt` FROM `Foos` AS `Foo`;
for-of
在 SQLite 中,
.forEach()
和 for-of
循环都可以正确更新记录。但是,在 MySQL 中,.forEach()
循环似乎不应用更新,而 for-of
循环则应用更新。
脚本的问题与 forEach 循环内的异步行为有关。在 JavaScript 中,forEach 无法正确处理异步操作,并且在进入下一次迭代之前它不会等待循环内的等待完成。这可能会导致意外结果或不完整的操作。查看 Using async/await with a forEach Loop 的答案以了解详细信息。
items.forEach(async item => {
item.bar = 'forEach';
await item.save();
});
您可以使用以下代码片段轻松修改它,结果应该按您的预期工作。
const promises = items.map(async (item) => {
item.bar = 'forEach';
await item.save();
});
await Promise.all(promises);