【问题标题】:Update or delete current row of database results更新或删除数据库结果的当前行
【发布时间】:2025-12-23 07:40:11
【问题描述】:

我正在尝试将一些旧的 VB6 代码移植到 C# 和 .NET。

在很多地方,旧代码使用RecordSet 来执行 SQL 查询,然后循环遍历结果。到目前为止没问题,但是在循环内部,代码会更改当前行,更新列,甚至完全删除当前行。

在 .NET 中,我可以轻松地使用 SqlDataReader 循环访问 SQL 查询结果,但不支持更新。

所以我一直在使用SqlDataAdapter 填充DataSet,然后循环遍历DataSet 表中的行。但是与 VB6 的旧 RecordSet 相比,DataSet 似乎不太聪明。一方面,我需要为我拥有的每种编辑类型提供更新查询。另一个问题是DataSet 似乎一次将所有内容都保存在内存中,如果有很多结果,这可能是个问题。

在 .NET 中复制此行为的最佳方法是什么?下面的代码显示了我到目前为止所拥有的。这是最好的方法,还是有其他选择?

using (SqlConnection connection = new SqlConnection(connectionString))
{
    DataSet dataset = new DataSet();
    using (SqlDataAdapter adapter = new SqlDataAdapter(new SqlCommand(query, connection)))
    {
        adapter.Fill(dataset);

        DataTable table = dataset.Tables[0];
        foreach (DataRow row in table.Rows)
        {
            if ((int)row["Id"] == 4)
            {
                if ((int)row["Value1"] > 0)
                    row["Value2"] = 12345;
                else
                    row["Value3"] = 12345;
            }
            else if ((int)row["Id"] == 5)
            {
                 row.Delete();
            }
        }

        // TODO:
        adapter.UpdateCommand = new SqlCommand("?", connection);
        adapter.DeleteCommand = new SqlCommand("?", connection);

        adapter.Update(table);
    }
}

注意:我是公司的新手,不能很好地告诉他们必须更改连接字符串或必须切换到实体框架,这将是我的选择。我真的在寻找纯代码解决方案。

【问题讨论】:

  • 您使用的是什么数据库?如果您使用的是 Sql Server,您只需 enable Multiple Active Result Sets
  • @Steve:SQL Server,但我原以为这是由 ADO.NET 决定的。
  • 如上,只需在连接字符串中使用Multiple Active Result Sets
  • 嗯,我是公司的新手,无法告诉他们必须使用什么连接。此外,即使使用该设置,我也无法像旧代码那样更新当前行。我必须运行单独的查询。
  • 是的,您需要执行单独的查询。 MARS 只允许您使用相同的连接和两个不同的操作。另一种可能性是打开第二个连接来更新行,但您仍然需要执行单独的查询。

标签: c# .net vb6 ado.net sqldataadapter


【解决方案1】:

ADO.NET DataTableDataAdapter 提供与 ADO Recordset 最接近的等效项,并应用了关注分离原则。 DataTable 包含数据并提供更改跟踪信息(类似于 EF 内部实体跟踪),而 DataAdapter 提供从数据库填充数据的标准方法(Fill 方法)并将更改应用回数据库(Update方法)。

话虽如此,您正在做的是将 ADO Recordset 移植到 ADO.NET 的预期方式。您唯一遗漏的是,您并不总是需要指定InsertUpdateDelete 命令。一旦您的查询正在查询单个表(我认为这是获得可更新Recordset 的要求),您可以使用另一个名为DbCommandBuilder 的ADO.NET 播放器:

自动生成单表命令,用于协调对 DataSet 所做的更改与关联的数据库。

每个数据库提供者都提供这个抽象类的实现。 MSDN example for SqlCommandBuilder 与您的示例几乎相同,因此在调用 Update 之前您只需要(有点违反直觉):

var builder = new SqlCommandBuilder(adapter); 

就是这样。

幕后,

DbCommandBuilder 将自己注册为由该属性中指定的 DbDataAdapter 生成的 RowUpdating 事件的侦听器。

如果您没有在数据适配器中专门设置命令,则动态生成命令。

【讨论】:

  • 这似乎做了一些类似于我的建议的事情,只是开箱即用,用户部分的努力更少......令人讨厌的是我知道它存在,我以前曾使用过它,但是忘记了...哦,好吧,+1 :-)
  • 对于解决问题的核心问题和提供足够简单的解决方案,这显然是明确的答案,我相信这就是微软打算解决我想要解决的问题的方式做。初步测试证实这符合预期。我现在唯一需要弄清楚的是如何正确处理SqlConnectionSqlDataAdapterSqlCommandDataSetDataSetSqlCommandBuilder 的所有IDisposable 接口,而不会使代码看起来一团糟。跨度>
  • 一般来说你必须这样做。但实际上在 ADO.NET 中只有 3 个类需要您处理 - 连接、事务和读取器。使用这种类型的用法 - 仅连接。如果你没有明确地打开它(看起来),甚至没有连接。 FillUpdate 方法将为您完全处理连接/事务/读取器操作,类似于 EF DbContextIQueryable 执行和 SaveChanges 方法。
  • @IvanStoev:顺便说一句,您似乎对此了解很多。您能帮我了解一下内存使用情况吗?似乎SqlDataReader() 不会将所有结果加载到内存中,但SqlDataAdapter.Fill() 可能。你能确认一下吗?
  • @JonathanWood 当然 - 确认。 DataReader 相当于只进只读数据库游标(或Recordset)。虽然DataTable 相当于只读快照模式Recordset(抱歉,如果我没有使用正确的术语,我们使用VB6 已经很长时间了:))所以是的,DataTable 缓冲了整个查询。但请注意,没有真正等同于可更新的非缓存 Recordset - DataReader 只允许您阅读,甚至 EF 也需要加载该内容才能删除/更新它,这会导致加载行为类似于DataTable
【解决方案2】:

我想出了一个(未经测试的)数据表解决方案。 它确实需要您做一些工作,但它应该为您更改或删除的每一行自动生成更新和删除命令,方法是连接到DataTableRowChangedRowDeleted 事件。

每一行都会得到它自己的命令,相当于ADODB.RecordSet更新/删除方法。

但是,与ADODB.RecordSet 方法不同,此类不会更改底层数据库,而只会创建 SqlCommands 来执行此操作。当然,您可以将其更改为在创建它们后简单地执行它们,但正如我所说,我没有对其进行测试,因此如果您想这样做,我将由您决定。但是,请注意我不确定RowChanged 事件对于同一行的多次更改将如何表现。在最坏的情况下,它将在行中的每个更改都被触发。

类构造函数接受三个参数:

  1. 您正在使用的 DataTable 类的实例。
  2. Dictionary<string, SqlDbType> 提供列名和 SqlDataTypes 之间的映射
  3. 表示表名的可选字符串。如果省略,将使用DataTableTableName 属性。

一旦您有了映射字典,您所要做的就是实例化CommandGenerator 类并像问题中一样迭代数据表中的行。从那时起,一切都是自动化的。

一旦你完成了你的迭代,你所要做的就是从Commands属性中获取sql命令,然后运行它们。

public class CommandGenerator
{
    private Dictionary<string, SqlDbType> _columnToDbType;
    private string _tableName;
    private List<SqlCommand> _commands;

    public CommandGenerator(DataTable table, Dictionary<string, SqlDbType> columnToDbType, string tableName = null)
    {
        _commands = new List<SqlCommand>();
        _columnToDbType = columnToDbType;
        _tableName = (string.IsNullOrEmpty(tableName)) ? tableName : table.TableName;

        table.RowDeleted += table_RowDeleted;
        table.RowChanged += table_RowChanged;
    }

    public IEnumerable<SqlCommand> Commands { get { return _commands; } }

    private void table_RowChanged(object sender, DataRowChangeEventArgs e)
    {
        _commands.Add(GenerateDelete(e.Row));
    }

    private void table_RowDeleted(object sender, DataRowChangeEventArgs e)
    {
        _commands.Add(GenerateDelete(e.Row));
    }

    private SqlCommand GenerateUpdate(DataRow row)
    {

        var table = row.Table;
        var cmd = new SqlCommand();
        var sb = new StringBuilder();
        sb.Append("UPDATE ").Append(_tableName).Append(" SET ");

        var valueColumns = table.Columns.OfType<DataColumn>().Where(c => !table.PrimaryKey.Contains(c));

        AppendColumns(cmd, sb, valueColumns, row);
        sb.Append(" WHERE ");
        AppendColumns(cmd, sb, table.PrimaryKey, row);

        cmd.CommandText = sb.ToString();
        return cmd;
    }

    private SqlCommand GenerateDelete(DataRow row)
    {
        var table = row.Table;
        var cmd = new SqlCommand();
        var sb = new StringBuilder();
        sb.Append("DELETE FROM ").Append(_tableName).Append(" WHERE ");
        AppendColumns(cmd, sb, table.PrimaryKey, row);
        cmd.CommandText = sb.ToString();
        return cmd;
    }

    private void AppendColumns(SqlCommand cmd, StringBuilder sb, IEnumerable<DataColumn> columns, DataRow row)
    {
        foreach (var column in columns)
        {
            sb.Append(column.ColumnName).Append(" = @").AppendLine(column.ColumnName);
            cmd.Parameters.Add("@" + column.ColumnName, _columnToDbType[column.ColumnName]).Value = row[column];
        }
    }
}

正如我所写,这是完全未经测试的,但我认为它至少应该足以展示总体思路。

【讨论】:

    【解决方案3】:

    您的限制:

    • 不使用实体框架

    • DataSet 似乎一次将所有内容都保存在内存中,这可能是 如果有很多结果就会出现问题。

    • 纯代码解决方案(无外部库)

    加号

    • DataTable 可以存储的最大行数为 16,777,216 行MSDN

    • 为了获得高性能

      //the main class to update/delete sql batches without using DataSet/DataTable.
      
      
      public class SqlBatchUpdate
          {
              string ConnectionString { get; set; }
              public SqlBatchUpdate(string connstring)
              {
                  ConnectionString = connstring;
              }
      
              public int RunSql(string sql)
              {
                  using (SqlConnection con = new SqlConnection(ConnectionString))
                  using (SqlCommand cmd = new SqlCommand(sql, con))
                  {
                      cmd.CommandType = CommandType.Text;
                      con.Open();
                      int rowsAffected = cmd.ExecuteNonQuery();
                      return rowsAffected;
                  }
              }
          }
      
      //------------------------
      // using the class to run a predefined patches
      
       public class SqlBatchUpdateDemo
          {       
             private string connstring = "myconnstring";
      
             //run batches in sequence
          public void RunBatchesInSequence()
          {
              var sqlBatchUpdate = new SqlBatchUpdate(connstring);
      
              //batch1
              var sql1 = @"update mytable set value2 =1234 where id =4  and Value1>0;";
              var nrows = sqlBatchUpdate.RunSql(sql1);
              Console.WriteLine("batch1: {0}", nrows);
      
              //batch2
              var sql2 = @"update mytable set value3 =1234 where      id =4   and Value1 =0";
                nrows = sqlBatchUpdate.RunSql(sql2);
              Console.WriteLine("batch2: {0}", nrows);
      
              //batch3
              var sql3 = @"delete from  mytable where id =5;";
                nrows = sqlBatchUpdate.RunSql(sql3);
              Console.WriteLine("batch3: {0}", nrows);
      
      
          }
      
              // Alternative: you can run all batches as one 
                  public void RunAllBatches()
                  {
                      var sqlBatchUpdate = new SqlBatchUpdate(connstring );
                      StringBuilder sb = new StringBuilder();
                      var sql1 = @"update mytable set value2 =1234 where id =4  and Value1>0;";
                      sb.AppendLine(sql1);
      
                      //batch2
                      var  sql2 = @"update mytable set value3 =1234 where     id =4   and Value1 =0";
                      sb.AppendLine(sql2);
      
                      //batch3
                     var sql3 = @"delete from  mytable where  id =5;";
                      sb.AppendLine(sql3);
      
                      //run all batches
                      var   nrows = c.RunSql(sb.ToString());
                      Console.WriteLine("all patches: {0}", nrows);
                  }
      
      
          }
      

    我模拟了那个解决方案,它运行良好,性能很高,因为所有更新/删除都作为批处理运行。

    【讨论】: