一次执行多个 SQL 语句到 SQL Server
本文介绍了一些技术,说明如何使用单个 SqlCommand 执行多个 SQL 语句。
引言
ADO.NET 在 SQL Server 中一个经常被忽视的功能是它能够使用单个 SqlCommand
执行多个 SQL 语句。很多时候,程序会单独执行语句,或者调用执行大量语句的存储过程。当然,使用存储过程是首选方式,但有时通过一次调用执行多个语句会更有益。这可以通过“批处理”来实现,批处理基本上是将一组 SQL 或 T-SQL 语句放在一起。
设置
为了测试该功能,让我们创建一个小型表。
---------------------------------
-- Create the test table
---------------------------------
CREATE TABLE MultiStatementTest (
id int not null identity(1,1),
somevalue int not null
);
并向其中插入几行数据。
---------------------------------
-- Add a few rows
---------------------------------
DECLARE @counter int = 1
BEGIN
WHILE (@counter <= 5) BEGIN
INSERT INTO MultiStatementTest (somevalue) VALUES (RAND() * 1000);
SET @counter = @counter + 1;
END;
END;
现在数据看起来大概是这样
---------------------------------
-- Select the initial data
---------------------------------
SELECT * FROM MultiStatementTest;
id somevalue
--- ---------
1 854
2 73
3 732
4 546
5 267
测试程序
测试程序非常易于使用。只需为创建了测试表的数据库定义正确的连接字符串,即可运行测试。
执行多个 SQL 语句
第一个方法使用 SqlCommand.ExecuteNonQuery
对测试表执行两个单独的 SQL 语句。第一个语句将 `somevalue` 字段加一,第二个语句加二。该方法如下所示:
/// <summary>
/// Executes two separate updates against the the connection
/// </summary>
/// <param name="connectionString">Connection string to use</param>
/// <param name="generateError">Should the statement generate an error</param>
/// <returns>True if succesful</returns>
public static bool ExecuteMultipleUpdates(string connectionString, bool generateError = false) {
System.Data.SqlClient.SqlConnection connection = new System.Data.SqlClient.SqlConnection();
System.Data.SqlClient.SqlCommand command = new System.Data.SqlClient.SqlCommand();
int rowsAffected;
connection.ConnectionString = connectionString;
command.CommandText = @"
UPDATE MultiStatementTest SET somevalue = somevalue + 1;
UPDATE MultiStatementTest SET" + (generateError ? "WONTWORK" : "") +
" somevalue = somevalue + 2;";
command.CommandType = System.Data.CommandType.Text;
command.Connection = connection;
try {
connection.Open();
rowsAffected = command.ExecuteNonQuery();
} catch (System.Exception exception) {
System.Windows.MessageBox.Show(exception.Message, "Error occurred");
return false;
} finally {
command.Dispose();
connection.Dispose();
}
System.Windows.MessageBox.Show(string.Format("{0} rows updated",
rowsAffected, "Operation succesful"));
return true;
}
因此,CommandText
属性包含将在该批处理中执行的所有语句。语句之间用分号分隔。
批处理执行完毕后,行已更新两次,因此表的内容看起来大概是这样:
id somevalue
--- ---------
1 857
2 76
3 735
4 549
5 270
需要注意的一个重要事项是,ExecuteNonQuery
返回的影响行数为 10。表中共有五行,每行都更新了两次,因此总更新次数为 10。因此,即使使用批处理,也可以检查是否正确地更新了行数,无论哪个语句执行了更新。
使用数据读取器执行两个 SELECT 语句
下一个测试是执行两个不同的 SELECT
语句,并使用 SqlDataReader
类读取结果。方法如下:
/// <summary>
/// Executes two separate select statements against the the connection using data reader
/// </summary>
/// <param name="connectionString">Connection string to use</param>
/// <param name="generateError">Should the statement generate an error</param>
/// <returns>True if succesful</returns>
public static bool ExecuteReader(string connectionString, bool generateError = false) {
System.Data.SqlClient.SqlConnection connection = new System.Data.SqlClient.SqlConnection();
System.Data.SqlClient.SqlCommand command = new System.Data.SqlClient.SqlCommand();
System.Data.SqlClient.SqlDataReader dataReader;
System.Text.StringBuilder stringBuilder;
bool loopResult = true;
connection.ConnectionString = connectionString;
command = new System.Data.SqlClient.SqlCommand();
command.CommandText = @"
SELECT somevalue FROM MultiStatementTest WHERE somevalue%2 = 1;
SELECT somevalue FROM MultiStatementTest " + (generateError ? "WONTWORK" : "WHERE") +
" somevalue%2 = 0;";
command.CommandType = System.Data.CommandType.Text;
command.Connection = connection;
try {
connection.Open();
dataReader = command.ExecuteReader();
while (loopResult) {
stringBuilder = new System.Text.StringBuilder();
while (dataReader.Read()) {
stringBuilder.AppendLine(dataReader.GetInt32(0).ToString());
}
System.Windows.MessageBox.Show(stringBuilder.ToString(), "Data from the result set");
loopResult = dataReader.NextResult();
}
} catch (System.Exception exception) {
System.Windows.MessageBox.Show(exception.Message, "Error occurred");
return false;
} finally {
command.Dispose();
connection.Dispose();
}
return true;
}
批处理中的思路相同,两个语句用分号分隔。在此示例中,行根据数字是奇数还是偶数被分为两个结果集。当调用 ExecuteReader
时,第一个结果集是自动可用的。该方法遍历行并显示结果:
857
735
549
要获取下一个结果,必须指示读取器使用 NextResult
方法前进到下一个结果集。之后,可以再次遍历第二组值。第二组的值的结果:
76
270
使用 SqlDataAdapter 处理多个 SELECT 语句
如果结果需要存储在 DataSet
中,那么使用 SqlDataReader
通常会很麻烦。在下一个测试中,我们将使用 SqlDataAdapter
来填充数据集。代码如下:
/// <summary>
/// Executes two separate select statements against the the connection
/// </summary>
/// <param name="connectionString">Connection string to use</param>
/// <param name="generateError">Should the statement generate an error</param>
/// <returns>True if succesful</returns>
public static bool ExecuteMultipleSelects(string connectionString, bool generateError = false) {
System.Data.SqlClient.SqlConnection connection = new System.Data.SqlClient.SqlConnection();
System.Data.SqlClient.SqlCommand command = new System.Data.SqlClient.SqlCommand();
System.Data.SqlClient.SqlDataAdapter adapter = new System.Data.SqlClient.SqlDataAdapter();
System.Data.DataSet dataset = new System.Data.DataSet();
connection.ConnectionString = connectionString;
command = new System.Data.SqlClient.SqlCommand();
command.CommandText = @"
SELECT * FROM MultiStatementTest WHERE somevalue%2 = 1;
SELECT " + (generateError ? "WONTWORK" : "*") +
" FROM MultiStatementTest WHERE somevalue%2 = 0;";
command.CommandType = System.Data.CommandType.Text;
command.Connection = connection;
try {
connection.Open();
adapter.SelectCommand = command;
adapter.Fill(dataset);
} catch (System.Exception exception) {
System.Windows.MessageBox.Show(exception.Message, "Error occurred");
return false;
} finally {
command.Dispose();
connection.Dispose();
}
System.Windows.MessageBox.Show(string.Format(
"Dataset contains {0} tables, {1} rows in table 1 and {2} rows in table 2",
dataset.Tables.Count,
dataset.Tables[0].Rows.Count,
dataset.Tables[1].Rows.Count,
"Operation succesful"));
return true;
}
现在这样获取数据确实很简单。代码只需调用适配器的 Fill
方法,并将 DataSet
作为参数传递。适配器会自动在数据集中创建两个单独的 DataTable
对象并填充它们。在我的测试场景中,第一个表包含三行,第二个表包含两行。
由于在此示例中表是即时创建的,因此它们被自动命名为 Table1
和 Table2
,所以如果使用名称引用表,则更改为更具描述性的名称是明智的。
执行匿名 T-SQL 块
尽管存储过程非常出色,但有时 T-SQL 代码可能具有高度动态性。在这种情况下,创建存储过程可能很困难。批处理也可以用来执行一组 T-SQL 语句。在这种方法中,数据库中没有命名对象,但批处理会像从 SQL Server Management Studio 执行一样被执行。
测试代码如下:
/// <summary>
/// Executes an anonymous T-SQL batch against the the connection
/// </summary>
/// <param name="connectionString">Connection string to use</param>
/// <param name="generateError">Should the statement generate an error</param>
/// <returns>True if succesful</returns>
public static bool ExecuteAnonymousTSql(string connectionString, bool generateError = false) {
System.Data.SqlClient.SqlConnection connection = new System.Data.SqlClient.SqlConnection();
System.Data.SqlClient.SqlCommand command = new System.Data.SqlClient.SqlCommand();
int rowsAffected;
connection.ConnectionString = connectionString;
command.CommandText = @"
DECLARE @counter int = 1
BEGIN
WHILE (@counter <= 5) BEGIN
INSERT INTO MultiStatementTest (somevalue) VALUES (RAND() * 100000);
SET @counter = @counter + 1;
" + (generateError ? "WONTWORK" : "") + @"
END;
END;";
command.CommandType = System.Data.CommandType.Text;
command.Connection = connection;
try {
connection.Open();
rowsAffected = command.ExecuteNonQuery();
} catch (System.Exception exception) {
System.Windows.MessageBox.Show(exception.Message, "Error occurred");
return false;
} finally {
command.Dispose();
connection.Dispose();
}
System.Windows.MessageBox.Show(string.Format("{0} rows inserted",
rowsAffected,
"Operation succesful"));
return true;
}
现在,在此示例中,使用的是与开始时创建测试行相同的脚本。您可以看到,变量声明、循环等都是可以包含在批处理中的有效语句。
运行此代码后,表中将添加五行。另外请注意,由于 NOCOUNT
处于关闭状态(默认),ExecuteNonQuery
方法将返回批处理中插入的正确行数。如果 NOCOUNT
设置为开启,则受影响的行数将为 -1。
错误处理怎么样?
如果在批处理中执行单个语句时发生错误怎么办?我使用了一些语法错误来测试这一点。批处理被解析为一个整体,因此即使后面的语句包含语法错误,批处理也不会产生任何影响。例如,如果批处理中包含错误的 UPDATE
语句,则表的状态不会改变。
如果错误不是语法错误而是执行过程中发生的错误,情况则不同。例如,考虑外键错误,当出现不正确的值时会检测到。在这种情况下,前面的语句可能已经更改了数据库状态(取决于语句),因此像往常一样,使用适当的事务是明智的。
结论
虽然批处理不能(也不应该)取代旧的存储过程等,但正确使用它们非常有用。它们可用于创建非常动态的操作,例如,而无需进行多次往返,只要在调用之间不需要客户端逻辑。
历史
- 2011 年 12 月 27 日:创建文章。