使用Tedious与Node.js向SQL Server批量保存数组的异步循环问题
解决SQL Server批量插入时的RequestError(LoggedInSendingInitialSql状态异常)
问题根源
你遇到的错误是因为Tedious连接尚未完全进入LoggedIn就绪状态时就发起了SQL请求。当前代码在循环中批量创建Connection实例,每个连接的connect是异步操作——当connect事件触发时,连接可能还在执行初始化SQL的过程中(处于LoggedInSendingInitialSql状态),此时调用execSql就会触发状态异常。此外,每个请求新建连接的方式不仅低效,还容易引发连接资源竞争问题。
推荐解决方案:使用连接池管理连接
Tedious支持连接池机制,既能避免重复创建连接的开销,又能确保连接处于就绪状态再执行请求,同时配合参数化查询杜绝SQL注入风险。
步骤1:安装并初始化连接池
先安装连接池依赖:
npm install tedious-connection-pool
然后初始化连接池:
const ConnectionPool = require('tedious-connection-pool'); const { Request, TYPES } = require('tedious'); const { v4: uuidv4 } = require('uuid'); // 你的数据库配置 const dbConfig = { server: '你的SQL Server地址', authentication: { type: 'default', options: { userName: '账号', password: '密码' } }, options: { database: '目标数据库', encrypt: true // 根据你的SQL Server配置调整 } }; // 连接池配置 const poolConfig = { min: 2, // 最小空闲连接数 max: 10, // 最大连接数 log: false // 关闭日志,调试时可开启 }; const pool = new ConnectionPool(poolConfig, dbConfig);
步骤2:改造插入函数(使用参数化查询)
将字符串拼接的SQL改为参数化查询,同时正确封装Promise:
function addNewRoom(pool, floorID, newroomname) { return new Promise((resolve, reject) => { // 从连接池获取连接 pool.acquire((err, connection) => { if (err) { console.error('获取连接失败:', err.message); return reject(err); } const roomID = uuidv4(); // 参数化SQL语句 const request = new Request( `INSERT INTO [dbo].[room] (room_id, floor_id, room_name, time_created, time_modified) VALUES (@roomID, @floorID, @roomName, GETDATE(), GETDATE());`, (err, rowCount) => { connection.release(); // 执行完成后释放连接回池 if (err) { console.error('插入失败:', err.message); return reject(err); } console.log(`${rowCount}条记录插入成功`); resolve(rowCount); } ); // 添加参数(根据字段类型选择对应的TYPES) request.addParameter('roomID', TYPES.UniqueIdentifier, roomID); request.addParameter('floorID', TYPES.UniqueIdentifier, floorID); request.addParameter('roomName', TYPES.NVarChar, newroomname); connection.execSql(request); }); }); }
步骤3:批量插入的循环控制
使用async/await控制插入流程,可选择串行或并行执行:
串行执行(适合数据量小,避免数据库压力)
async function batchInsertRooms(floorID, roomNameList) { for (const roomName of roomNameList) { try { await addNewRoom(pool, floorID, roomName); } catch (err) { console.error(`插入房间${roomName}失败:`, err.message); // 可选:若需中断流程,取消注释下面的代码 // throw err; } } // 所有插入完成后关闭连接池(若后续不再使用) pool.drain(); } // 调用执行 batchInsertRooms(floorID, newroomname);
并行执行(适合数据量大,控制并发数)
async function batchInsertRoomsParallel(floorID, roomNameList) { // 生成所有插入Promise const insertPromises = roomNameList.map(roomName => addNewRoom(pool, floorID, roomName) .catch(err => console.error(`插入房间${roomName}失败:`, err.message)) ); // 等待所有Promise完成 await Promise.all(insertPromises); pool.drain(); } // 调用执行 batchInsertRoomsParallel(floorID, newroomname);
备选方案:不使用连接池时的修复
若不想用连接池,需确保连接完全进入LoggedIn状态后再执行SQL,同时完善Promise封装:
// 循环中修改连接监听逻辑 for (let i = 0; i < newroomname.length; i++) { const addroomconnection = new Connection(config); addroomconnection.on('connect', function (err) { if (err) { console.error('连接失败:', err.message); return; } // 等待连接进入就绪状态 waitForConnectionReady(addroomconnection) .then(() => addNewRoom(addroomconnection, floorID, newroomname[i])) .then(() => addroomconnection.close()) .catch(err => console.error('处理失败:', err.message)); }); addroomconnection.connect(); } // 辅助函数:等待连接进入LoggedIn状态 function waitForConnectionReady(connection) { return new Promise((resolve) => { if (connection.state === 'LoggedIn') { return resolve(); } const checkState = () => { if (connection.state === 'LoggedIn') { connection.removeListener('stateChange', checkState); resolve(); } }; connection.on('stateChange', checkState); }); } // 完善addNewRoom的Promise封装 function addNewRoom(addroomconnection, floorID, newroomname) { return new Promise((resolve, reject) => { console.log("开始插入房间记录..."); const roomID = uuidv4(); const request = new Request( `INSERT INTO [dbo].[room] (room_id, floor_id, room_name, time_created, time_modified) VALUES ('${roomID}', '${floorID}', '${newroomname}', GETDATE(), GETDATE());`, (err, rowCount) => { if (err) { console.error(err.message); reject(err); } else { console.log(`${rowCount}条记录插入成功`); resolve(rowCount); } } ); addroomconnection.execSql(request); }); }
关键注意事项
- 杜绝SQL注入:永远不要用字符串拼接生成SQL语句,必须使用参数化查询。
- 连接复用:重复创建连接是低效且易出问题的,优先使用连接池管理连接资源。
- Promise正确封装:确保异步操作的Promise能正确
resolve和reject,避免出现永久pending的状态。
内容的提问来源于stack exchange,提问作者SadCoder
相关产品推荐
相关产品推荐

