C#保存SQL实体的方法能否返回插入行ID?如何设计返回值?
解决方案
针对你想让SavePhoneAsync返回插入实体Id的需求,同时解决SRP顾虑和返回值歧义问题,以下是几种可行的处理方式:
方案1:修改方法命名+返回插入Id(推荐)
直接调整方法名和逻辑,让方法职责清晰,避免返回值歧义:
- 重命名方法为
InsertPhoneAndGetIdAsync,明确告知调用者该方法的行为是插入Phone并返回其生成的Id; - 修改SQL语句,追加获取自增Id的逻辑(SQL Server用
SCOPE_IDENTITY()); - 扩展数据访问层方法,支持返回执行结果而非仅受影响行数。
代码调整示例
首先修改SqlServerDataAccess的泛型方法,支持返回执行结果:
public class SqlServerDataAccess : ISqlDataAccess { // 新增支持返回结果的泛型方法 public async Task<TResult> ExecuteScalarAsync<TParams, TResult>(string sqlStatement, TParams parameters, string connectionString) { using IDbConnection connection = new SqlConnection(connectionString); return await connection.ExecuteScalarAsync<TResult>(sqlStatement, parameters); } // 保留原有返回受影响行数的方法(供其他场景使用) public async Task<int> SaveDataAsync<T>(string sqlStatement, T parameters, string connectionString) { using IDbConnection connection = new SqlConnection(connectionString); return await connection.ExecuteAsync(sqlStatement, parameters); } }
然后修改SqlPhoneData中的方法:
public class SqlPhoneData : IPhoneData { // ... 原有构造函数省略 ... public async Task<int> InsertPhoneAndGetIdAsync(Phone phone) { string sqlStatement = "INSERT INTO dbo.Phone (Number) VALUES (@Number);" + "SELECT SCOPE_IDENTITY();"; DynamicParameters parameters = new(phone); return await _dataAccess.ExecuteScalarAsync<DynamicParameters, int>(sqlStatement, parameters, _connectionString); } }
方案说明
- 完全解决返回值歧义:方法名直接说明行为,调用者不会把返回值误认为受影响行数;
- 不违反SRP:“插入实体并获取其自增Id”是一个连贯的业务操作,属于单一职责范畴——SRP禁止的是一个方法处理完全无关的职责(比如同时插入数据和发送通知),而非同一个操作的必要后续步骤。
方案2:保留受影响行数返回,通过输出参数传递Id
如果业务场景确实需要保留受影响行数的返回,可以通过out参数或输出型SQL参数返回插入的Id:
代码示例
public async Task<int> SavePhoneAsync(Phone phone, out int insertedId) { string sqlStatement = "INSERT INTO dbo.Phone (Number) VALUES (@Number);" + "SET @InsertedId = SCOPE_IDENTITY();"; DynamicParameters parameters = new(phone); // 添加输出参数接收插入的Id parameters.Add("@InsertedId", dbType: DbType.Int32, direction: ParameterDirection.Output); int affectedRows = await _dataAccess.SaveDataAsync(sqlStatement, parameters, _connectionString); insertedId = parameters.Get<int>("@InsertedId"); return affectedRows; }
方案说明
- 同时满足获取受影响行数和插入Id的需求;
- 缺点是方法签名变复杂,调用者需要额外处理
out参数,建议配合清晰的XML注释说明参数用途。
方案3:拆分方法(不推荐)
如果严格追求SRP,尝试拆分出“插入”和“获取Id”两个独立方法,但必须用事务包裹避免并发问题:
代码示例
public async Task<int> InsertPhoneAndGetIdAsync(Phone phone) { using IDbConnection connection = new SqlConnection(_connectionString); await connection.OpenAsync(); using var transaction = await connection.BeginTransactionAsync(); try { // 执行插入 string insertSql = "INSERT INTO dbo.Phone (Number) VALUES (@Number);"; await connection.ExecuteAsync(insertSql, phone, transaction); // 获取当前事务内的插入Id string getIdSql = "SELECT SCOPE_IDENTITY();"; int insertedId = await connection.ExecuteScalarAsync<int>(getIdSql, transaction: transaction); await transaction.CommitAsync(); return insertedId; } catch { await transaction.RollbackAsync(); throw; } }
方案说明
- 严格遵循SRP,但代码复杂度大幅提升,且如果不用事务包裹,会存在并发场景下获取错误Id的风险;
- 仅适合对代码职责拆分有极端要求的场景,实际业务中没必要过度拆分。
内容的提问来源于stack exchange,提问作者Dake
相关产品推荐
相关产品推荐

