VS2010 C#+SQL Server2014多用户主从表事务保存SQL查询问题
嘿,你这个多用户场景下的主从表保存问题太典型了——之前好多人直接用MAX(SrNo)拿最新ID,结果并发的时候各种数据串错,头疼得很。结合你用的VS2010 C#和SQL Server2014,给你几个经过验证的靠谱方案:
方案1:用SCOPE_IDENTITY()获取当前会话的自增ID
这是最常用的解决办法,SCOPE_IDENTITY()只会返回当前会话、当前作用域内生成的自增值,完全不会被其他用户的操作干扰,完美规避并发冲突。
核心思路是:在同一个数据库连接和事务里,先插入主表,立刻调用SCOPE_IDENTITY()拿到刚生成的SrNo,再用这个ID关联插入明细表。
C#代码示例(用SqlCommand实现):
using (SqlConnection conn = new SqlConnection("你的数据库连接字符串")) { conn.Open(); // 开启事务,确保主从表操作要么全成要么全滚 using (SqlTransaction tran = conn.BeginTransaction()) { try { // 插入主表并返回刚生成的SrNo string insertHeaderSql = @" INSERT INTO Header (往来单位编码, 往来单位名称, 事务号, 日期) VALUES (@CustomerCode, @CustomerName, @TransNo, @TransDate); SELECT SCOPE_IDENTITY();"; SqlCommand headerCmd = new SqlCommand(insertHeaderSql, conn, tran); // 替换成你的实际参数值 headerCmd.Parameters.AddWithValue("@CustomerCode", "C0001"); headerCmd.Parameters.AddWithValue("@CustomerName", "XX商贸有限公司"); headerCmd.Parameters.AddWithValue("@TransNo", "TR20240520001"); headerCmd.Parameters.AddWithValue("@TransDate", DateTime.Now); // 获取主表的SrNo int headerSrNo = Convert.ToInt32(headerCmd.ExecuteScalar()); // 插入明细表,用刚才拿到的SrNo关联 string insertDetailSql = @" INSERT INTO Detail (SrNo, 商品编码, 商品名称, 单位, 单价, 数量) VALUES (@HeaderSrNo, @ProductCode, @ProductName, @Unit, @Price, @Qty);"; SqlCommand detailCmd = new SqlCommand(insertDetailSql, conn, tran); detailCmd.Parameters.AddWithValue("@HeaderSrNo", headerSrNo); // 如果有多条明细,这里可以循环执行 detailCmd.Parameters.AddWithValue("@ProductCode", "P0001"); detailCmd.Parameters.AddWithValue("@ProductName", "XX办公椅"); detailCmd.Parameters.AddWithValue("@Unit", "把"); detailCmd.Parameters.AddWithValue("@Price", 299.00); detailCmd.Parameters.AddWithValue("@Qty", 10); detailCmd.ExecuteNonQuery(); // 提交事务 tran.Commit(); } catch (Exception ex) { // 出错回滚,避免数据不一致 tran.Rollback(); throw ex; // 或者根据业务需求处理异常 } } }
⚠️ 注意:必须在同一个连接和事务里执行插入主表和获取SCOPE_IDENTITY()的操作,不然可能拿到其他会话生成的ID。
方案2:用OUTPUT子句直接返回插入的自增ID
这个方法更灵活,如果你需要同时返回主表的多个字段(比如SrNo+事务号),用OUTPUT子句会更方便。
SQL插入主表的语句可以改成:
INSERT INTO Header (往来单位编码, 往来单位名称, 事务号, 日期) OUTPUT inserted.SrNo -- 返回刚插入的SrNo VALUES (@CustomerCode, @CustomerName, @TransNo, @TransDate);
C#里同样用ExecuteScalar()获取这个值,和方案1的逻辑一致;如果要返回多个字段,就写成OUTPUT inserted.SrNo, inserted.事务号,再用ExecuteReader()读取即可。
方案3:绕过自增列,提前生成全局唯一事务号(可选)
如果事务号是业务层面的唯一标识,也可以考虑在插入主表前就生成全局唯一的事务号,这样主从表直接用事务号关联,彻底摆脱对自增列的依赖。
比如用SQL Server的序列对象(SQL Server 2012+支持,你的2014完全没问题):
-- 先创建一个序列(只需要执行一次) CREATE SEQUENCE TransNoSeq START WITH 1 INCREMENT BY 1; -- 插入主表前获取序列值作为事务号 DECLARE @NewTransNo VARCHAR(20); SET @NewTransNo = 'TR' + CAST(NEXT VALUE FOR TransNoSeq AS VARCHAR(10)); -- 插入主表 INSERT INTO Header (往来单位编码, 往来单位名称, 事务号, 日期) VALUES (@CustomerCode, @CustomerName, @NewTransNo, @TransDate); -- 用事务号关联插入明细表 INSERT INTO Detail (事务号, 商品编码, 商品名称, 单位, 单价, 数量) VALUES (@NewTransNo, @ProductCode, @ProductName, @Unit, @Price, @Qty);
这个方法的优势是:NEXT VALUE FOR是原子操作,并发下绝对不会重复,事务号可以提前生成给前端展示,不需要等主表插入完成。
避坑提醒
- 绝对不要用
@@IDENTITY:它会返回整个数据库范围内最后生成的自增ID,包括其他表的触发器生成的ID,很容易串错数据。 - 绝对不要用
MAX(SrNo):多用户并发时,你查到的最大ID可能已经被其他用户占用,直接导致主从表关联错误。 - 一定要用事务包裹主从表操作:确保主表和明细表的插入要么都成功,要么都回滚,避免出现“主表有数据,明细表为空”的不一致情况。
内容的提问来源于stack exchange,提问作者Chintan Shah

