
本文详解 node.js 中 mysql 查询的异步特性导致的执行时序问题,通过回调嵌套与错误处理,确保 `updatescratchergamedata` 完成后再执行 `updatescratcherrowdata`,避免因外键未生成而引发的 `cannot read properties of undefined (reading 'game_id')` 错误。
JavaScript 默认以非阻塞、异步方式执行 I/O 操作(如数据库查询),这意味着 connection.query() 调用后会立即返回,后续代码(如 updateScratcherRowData(cleanData))不会等待查询完成就继续执行。这正是你遇到问题的根本原因:updateScratcherRowData 在 INSERT INTO game 尚未真正写入数据库前就发起 SELECT game_id 查询,结果 rows[0] 为 undefined,从而触发 TypeError。
✅ 正确做法:在插入完成的回调中触发下一流程
必须将 updateScratcherRowData(cleanData) 的调用严格置于 INSERT 成功执行的回调内部,确保数据已落库且可被后续查询命中。参考修正后的代码:
function updateScratcherGameData(cleanData) {
uniqueGameData.forEach(e => { // 推荐使用 forEach 替代 map(无返回值意图)
const insertGameData = `INSERT INTO game (name, scratcher_lotto_id, gametype_id, active) VALUES ('${e.name}', ${e.scratcher_id}, 1, 1);`;
connection.query(`SELECT name FROM game WHERE name = '${e.name}';`, (err, rows) => {
if (err) throw err;
if (rows.length === 0) {
// 关键:INSERT 完成后再调用下游函数
connection.query(insertGameData, (err, result) => {
if (err) {
console.error('Failed to insert game:', err);
throw err;
}
console.log(`Inserted game: ${e.name}`);
});
}
});
});
// ❌ 错误:此处调用仍属“同步位置”,无法保证所有 INSERT 已完成
// updateScratcherRowData(cleanData);
}但注意:上述写法仍存在并发竞态风险——多个 connection.query(insertGameData, ...) 并行执行,updateScratcherRowData 无法知道所有插入是否全部结束。更健壮的方案是:
✅ 推荐进阶方案:使用 Promise + async/await(现代标准)
首先封装 connection.query 为 Promise 版本:
立即学习“Java免费学习笔记(深入)”;
const util = require('util');
const queryAsync = util.promisify(connection.query).bind(connection);然后重写逻辑,确保串行插入 + 统一完成后调用下游:
async function updateScratcherGameData(cleanData) {
for (const e of uniqueGameData) {
try {
const [existing] = await queryAsync(`SELECT name FROM game WHERE name = ?`, [e.name]);
if (existing.length === 0) {
await queryAsync(
`INSERT INTO game (name, scratcher_lotto_id, gametype_id, active) VALUES (?, ?, 1, 1)`,
[e.name, e.scratcher_id]
);
console.log(`Inserted new game: ${e.name}`);
}
} catch (err) {
console.error(`Error inserting game ${e.name}:`, err);
throw err;
}
}
// ✅ 所有游戏插入确认完成,再执行关联数据插入
await updateScratcherRowData(cleanData);
}
async function updateScratcherRowData(cleanData) {
for (const e of cleanData) {
try {
const [rows] = await queryAsync(
`SELECT game_id FROM game WHERE name = ? AND scratcher_lotto_id = ?`,
[e.name, e.scratcher_id]
);
if (!rows || rows.length === 0) {
throw new Error(`Game not found for name=${e.name}, id=${e.scratcher_id}`);
}
await queryAsync(
`INSERT INTO scratcher_data (scrape_date, game_id, prize, odds, remaining_prize, total_prize)
VALUES (NOW(), ?, ?, ?, ?, ?)`,
[
rows[0].game_id,
e.prize,
e.odds,
e.tickets_left,
e.tickets_remaining
]
);
console.log(`Inserted scratcher data for ${e.name}`);
} catch (err) {
console.error(`Failed to insert scratcher row for ${e.name}:`, err);
throw err;
}
}
}⚠️ 重要注意事项
- SQL 注入风险:原始代码直接拼接字符串(如 '${e["name"]}')极易遭受 SQL 注入攻击。✅ 务必改用参数化查询(如 ? 占位符 + 参数数组),如上例所示。
- 事务保障:若需强一致性(例如:某游戏插入失败则整批回滚),应包裹在 START TRANSACTION / COMMIT / ROLLBACK 中。
- 性能权衡:async/await 串行执行较慢但逻辑清晰;如需高性能,可用 Promise.all() 并行插入,但需配合 Promise.allSettled() 或自定义完成计数器确保下游触发时机。
通过理解异步本质、合理组织回调或采用 Promise/async-await,即可彻底解决“外键未就绪”的时序问题,让数据流真正按业务逻辑顺序可靠执行。










