You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#保存SQL实体的方法能否返回插入行ID?如何设计返回值?

解决方案

针对你想让SavePhoneAsync返回插入实体Id的需求,同时解决SRP顾虑和返回值歧义问题,以下是几种可行的处理方式:

方案1:修改方法命名+返回插入Id(推荐)

直接调整方法名和逻辑,让方法职责清晰,避免返回值歧义:

  1. 重命名方法为InsertPhoneAndGetIdAsync,明确告知调用者该方法的行为是插入Phone并返回其生成的Id;
  2. 修改SQL语句,追加获取自增Id的逻辑(SQL Server用SCOPE_IDENTITY());
  3. 扩展数据访问层方法,支持返回执行结果而非仅受影响行数。

代码调整示例

首先修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 14:25:23