更新或删除数据库结果的当前行

Update or delete current row of database results

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

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

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

所以我一直在尝试使用 SqlDataAdapter 填充 DataSet,然后遍历 DataSet table 中的行。但与 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);
    }
}

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

您的约束条件:

  • 不使用Entity Framework

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

  • 一个code-only解决方案(没有外部库)

  • 一个DataTable最多可以存储16777216行 行 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);
                }
    
    
        }
    

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

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

每一行都有自己的命令,等同于ADODB.RecordSet更新/删除方法。

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

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

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

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

完成迭代后,您所要做的就是从 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];
        }
    }
}

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

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

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

Automatically generates single-table commands used to reconcile changes made to a DataSet with the associated database.

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

var builder = new SqlCommandBuilder(adapter); 

就是这样。

幕后,

The DbCommandBuilder registers itself as a listener for RowUpdating events that are generated by the DbDataAdapter specified in this property.

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