更新或删除数据库结果的当前行
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。
它确实需要你做一些工作,但它应该为你自动更改或删除的每一行生成更新和删除命令,方法是连接到 DataTable
的 RowChanged
和 RowDeleted
事件.
每一行都有自己的命令,等同于ADODB.RecordSet
更新/删除方法。
但是,与 ADODB.RecordSet
方法不同,此 class 不会更改底层数据库,而只会创建 SqlCommands 来执行此操作。当然,您可以将其更改为在创建它们后简单地执行它们,但正如我所说,我没有测试它,所以如果您想这样做,我会把它留给您。但是,请注意,我不确定 RowChanged
事件对于同一行的多次更改将如何表现。最坏的情况是行中的每个更改都会被触发。
class 构造函数接受三个参数:
- 您正在使用的
DataTable
class 的实例。
- 提供列名和 SqlDataTypes 之间映射的
Dictionary<string, SqlDbType>
- 一个可选的字符串来表示 table 名称。如果省略,将使用
DataTable
的 TableName
属性。
一旦你有了映射字典,你所要做的就是实例化 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 DataTable
和 DataAdapter
提供最接近 ADO Recordset
的等价物,并应用分离原则。 DataTable
包含数据并提供更改跟踪信息(类似于 EF 内部实体跟踪),而 DataAdapter
提供从数据库填充它的标准方法(Fill
方法)并将更改应用回数据库(Update
方法)。
话虽如此,您正在做的是将 ADO Recordset
移植到 ADO.NET 的预期方法。您唯一错过的是您并不总是需要指定 Insert
、Update
和 Delete
命令。一旦您的查询正在查询单个 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.
如果您没有在数据适配器中专门设置命令,则动态生成命令。
我正在尝试将一些旧的 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。
它确实需要你做一些工作,但它应该为你自动更改或删除的每一行生成更新和删除命令,方法是连接到 DataTable
的 RowChanged
和 RowDeleted
事件.
每一行都有自己的命令,等同于ADODB.RecordSet
更新/删除方法。
但是,与 ADODB.RecordSet
方法不同,此 class 不会更改底层数据库,而只会创建 SqlCommands 来执行此操作。当然,您可以将其更改为在创建它们后简单地执行它们,但正如我所说,我没有测试它,所以如果您想这样做,我会把它留给您。但是,请注意,我不确定 RowChanged
事件对于同一行的多次更改将如何表现。最坏的情况是行中的每个更改都会被触发。
class 构造函数接受三个参数:
- 您正在使用的
DataTable
class 的实例。 - 提供列名和 SqlDataTypes 之间映射的
Dictionary<string, SqlDbType>
- 一个可选的字符串来表示 table 名称。如果省略,将使用
DataTable
的TableName
属性。
一旦你有了映射字典,你所要做的就是实例化 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 DataTable
和 DataAdapter
提供最接近 ADO Recordset
的等价物,并应用分离原则。 DataTable
包含数据并提供更改跟踪信息(类似于 EF 内部实体跟踪),而 DataAdapter
提供从数据库填充它的标准方法(Fill
方法)并将更改应用回数据库(Update
方法)。
话虽如此,您正在做的是将 ADO Recordset
移植到 ADO.NET 的预期方法。您唯一错过的是您并不总是需要指定 Insert
、Update
和 Delete
命令。一旦您的查询正在查询单个 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.
如果您没有在数据适配器中专门设置命令,则动态生成命令。