
本文详解如何修复因 node.js 中 mysql 查询异步特性引发的“数据依赖未就绪”问题,通过回调嵌套与错误处理确保 `updatescratchergamedata` 完成插入后再执行 `updatescratcherrowdata`,避免读取 `undefined.game_id` 报错。
在 Node.js 中使用原生 mysql 模块(或类似回调风格驱动)时,所有数据库操作默认为异步非阻塞——这意味着 connection.query() 发起后,JS 引擎不会等待其完成,而是立即执行后续语句。这正是你遇到问题的根本原因:
// ❌ 错误模式:updateScratcherRowData 被立即调用,此时 INSERT 可能尚未提交
uniqueGameData.map(... => {
connection.query(SELECT..., (err, rows) => {
if (rows.length === 0) {
connection.query(INSERT...); // 异步执行,无等待
}
});
});
updateScratcherRowData(cleanData); // ⚠️ 此处已执行,但 INSERT 可能未完成!结果就是 updateScratcherRowData 在 game 表中查不到刚“计划插入”的记录,rows[0] 为 undefined,进而触发 TypeError: Cannot read properties of undefined (reading 'game_id')。
✅ 正确做法是:将后续依赖操作置于前序操作的成功回调内部,形成串行链式调用。如答案所示,需在 INSERT 的回调中确认写入成功后再触发 updateScratcherRowData:
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, (insertErr, result) => {
if (insertErr) {
console.error('Failed to insert game:', insertErr);
throw insertErr;
}
console.log(`Inserted game: ${e.name} (ID: ${result.insertId})`);
});
}
});
});
// ❌ 仍存在隐患:forEach 是同步循环,但每个 query 是异步的
// 因此此处 updateScratcherRowData 仍可能过早执行!
}⚠️ 注意:上述写法虽修复了单条记录的嵌套逻辑,但 uniqueGameData.forEach() 中多个异步 query 并发执行,updateScratcherRowData 仍可能在任意一条 INSERT 完成前就被调用。真正健壮的解法需确保全部游戏插入完成后再统一执行下游。
立即学习“Java免费学习笔记(深入)”;
推荐升级方案:使用 Promise + Promise.all()(兼容旧版 mysql 库)
// 封装 query 为 Promise
const queryAsync = (sql, params) => {
return new Promise((resolve, reject) => {
connection.query(sql, params, (err, results) => {
if (err) reject(err);
else resolve(results);
});
});
};
async function updateScratcherGameData(cleanData) {
const insertPromises = uniqueGameData.map(async e => {
const exists = await queryAsync(`SELECT game_id FROM game WHERE name = ? AND scratcher_lotto_id = ?`,
[e.name, e.scratcher_id]);
if (exists.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 game: ${e.name}`);
}
});
// ✅ 等待所有游戏检查/插入完成
await Promise.all(insertPromises);
console.log('All games ensured. Proceeding to scratcher_data...');
// ✅ 此时再执行依赖查询的操作
await updateScratcherRowData(cleanData);
}同时优化 updateScratcherRowData(防止单条失败中断全部)
async function updateScratcherRowData(cleanData) {
for (const e of cleanData) {
try {
const gameRes = await queryAsync(
`SELECT game_id FROM game WHERE name = ? AND scratcher_lotto_id = ?`,
[e.name, e.scratcher_id]
);
if (gameRes.length === 0) {
console.warn(`Game not found for ${e.name} - skipping`);
continue;
}
await queryAsync(
`INSERT INTO scratcher_data (scrape_date, game_id, prize, odds, remaining_prize, total_prize)
VALUES (NOW(), ?, ?, ?, ?, ?)`,
[gameRes[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 data for ${e.name}:`, err.message);
}
}
}重要安全提醒 ?
当前代码存在严重 SQL 注入风险(直接拼接用户数据到 SQL 字符串)。务必使用参数化查询(如上 Promise 示例中的 ? 占位符),永远不要用模板字符串或 + 拼接动态值。
总结
- 根本原因:Node.js 异步 I/O 导致操作时序不可控;
- 快速修复:将下游函数调用移至上游 INSERT 的回调内;
- 生产推荐:升级为 async/await + Promise.all(),提升可读性与可控性;
- 必做加固:全面采用参数化查询,杜绝 SQL 注入。
遵循以上结构,即可彻底解决跨表外键依赖引发的竞态问题。










