错误发生在这里:
SqlConnection connection = GetConnection();
SqlTransaction transaction = null;
try
{
connection.Open();
transaction = connection.BeginTransaction();
SqlCommand logCommand = new SqlCommand("Log before main command", connection); // <--- did not give the transaction to the command
logCommand.ExecuteNonQuery(); // <--- Exception: ExecuteNonQuery requires the command to have a transaction ...
string sql = "SELECT 1";
SqlCommand command = new SqlCommand(sql, connection, transaction);
int rows = command.ExecuteNonQuery();
logCommand = new SqlCommand("Log after main command", connection);
logCommand.ExecuteNonQuery(); // <--- Same error also would have happened here
// Other similar code
transaction.Commit();
command.Dispose();
}
catch { /* Rollback etc */ }
finally { /* etc */ }
发生这种情况的原因是,当一个连接加入事务时,该连接上的所有命令都在该事务中。换句话说,您不能有一个命令“退出”事务,因为事务适用于整个连接。
不幸的是,SqlClient API 有点误导,因为在调用 connection.BeginTransaction() 之后,您仍然必须将 SqlTransaction 提供给 SqlCommand。如果你没有明确地将事务交给命令,那么当你执行命令时,SqlClient 会责备你(“不要忘记告诉我我已经知道我在其中的事务!”)这是你看到的例外。
这种笨拙是一些人喜欢使用 TransactionScope 的原因之一,尽管我个人不喜欢 TransactionScope 用于非分布式事务,因为它的“隐式魔法”API 以及与异步的不良交互。
如果您不希望“日志”命令与主命令位于同一事务中,则必须为它们使用另一个连接,或者仅在该主命令期间保留事务:
try
{
connection.Open();
// No transaction
SqlCommand logCommand = new SqlCommand("select 'Log before main command'", connection);
logCommand.ExecuteNonQuery();
// Now create the transaction
transaction = connection.BeginTransaction();
string sql = "SELECT 1";
SqlCommand command = new SqlCommand(sql, connection, transaction);
int rows = command.ExecuteNonQuery();
transaction.Commit();
// Transaction is completed, now there is no transaction again
logCommand = new SqlCommand("select 'Log after main command'", connection);
logCommand.ExecuteNonQuery();
// Other similar code
command.Dispose();
}
//catch { /* Rollback etc */ }
finally
{
if (transaction != null)
{
transaction.Dispose();
}
}
如果您确实希望他们成为交易的一部分,则必须明确地将交易交给他们:
SqlConnection connection = GetConnection();
SqlTransaction transaction = null;
try
{
connection.Open();
transaction = connection.BeginTransaction();
SqlCommand logCommand = new SqlCommand("select 'Log before main command'", connection, /* here */ transaction);
logCommand.ExecuteNonQuery();
string sql = "SELECT 1";
SqlCommand command = new SqlCommand(sql, connection, transaction);
int rows = command.ExecuteNonQuery();
logCommand = new SqlCommand("select 'Log after main command'", connection, /* and here */ transaction);
logCommand.ExecuteNonQuery();
// Other similar code
transaction.Commit();
command.Dispose();
}
//catch { /* Rollback etc */ }
finally
{
if (transaction != null)
{
transaction.Dispose();
}
}