我正在构建一个API方法,其中包含一个字典,并尝试将数据插入到数据库中。数据本质上是通过外键强制执行的父/子类型关系拆分的。数据库结构是出于特定原因而设计的,不能更改。
我最初是使用Entity Framework编写的,但性能测试表明,由于需要进行所有查询和处理,因此需要大量请求。将所有数据发送到数据库并允许它确定应该插入哪些记录要快得多(我们说的是20-30分钟到20-30秒)。
这是我的问题:最初,我只是返回使用ExecuteNonQuery插入的记录数。容易,对吗?现在,我需要能够找出哪些父记录已成功插入子记录。所以,我一直在努力重构它以促进它。
为清楚起见,我对插入了哪些父记录不感兴趣 - 我只对哪些父记录插入引用所述父记录的新子记录感兴趣。通过这种方式,我可以通过与调用者传递给API的内容进行比较,告知API调用者哪些记录未成功插入。到目前为止,我能看到的最好的方法是使用子OUTPUT
查询中的INSERT
子句来获取插入的ParentID并将它们存储在表变量中。然后我可以在父表中查找ID并获取我的比较名称。但这需要使用读者,并且由于涉及多个SQL语句,所以发生了不好的事情。
当前呈现的代码导致以下例外情况:
无法执行事务操作,因为存在处理此事务的待处理请求。这个SqlTransaction已经完成;它不再可用。测试方法My.Long.Project.Name.UnitTest.UnitTestMethod抛出异常:System.InvalidOperationException:此SqlTransaction已完成;它不再可用了。
虽然修复这些异常很有价值,但我对解决它们并不像解决实际问题那样感兴趣。如果有一条不同的路径,我可以采取非常快的速度并提供我需要的输出,然后我会调查它。这是我的代码。我希望我打算做的很清楚,任何帮助/指导/建议都会受到赞赏。
using (Context dbContext = createDbInstance())
{
//Not happy about setting MultipleActiveResultSets
string conn = dbContext.Database.Connection.ConnectionString + ";MultipleActiveResultSets=True";
SqlCommand newInsertCmd = new SqlCommand {Connection = new SqlConnection(conn)};
//Set up input variables here, including a TPV
SqlDataReader reader;
List<string> results = new List<string>();
newInsertCmd.Connection.Open();
SqlTransaction sqlTran = newInsertCmd.Connection.BeginTransaction();
newInsertCmd.Transaction = sqlTran;
try
{
//The two insert statements work just fine. The other junk here (including the OUTPUT clause) is brand new
const string qryInsertTrans =
@"INSERT INTO Parent ([Name], [CreateDate])
SELECT n.Name, GETUTCDATE() [CreateDate]
FROM
@NewRecords n
LEFT JOIN Parent p ON n.Name = p.Name
WHERE
p.ParentID IS NULL;
DECLARE @OutputVar table(
ParentID bigint NOT NULL
);
INSERT INTO Child ([ParentID], [SomeText], [CreateDate])
OUTPUT INSERTED.ParentID INTO @OutputVar
SELECT p.ParentID, n.Text, GETUTCDATE() [CreateDate]
FROM
@NewRecords n
INNER JOIN Parent p ON n.Name = p.Name
LEFT JOIN Child c ON p.ParentID = c.ParentID AND c.SomeCol = @SomeVal
WHERE
c.ChildID IS NULL;
SELECT p.Name
FROM Parent p INNER JOIN @OutputVar o ON p.ParentID = o.ParentID";
newInsertCmd.CommandText = qryInsertTrans;
reader = await newInsertCmd.ExecuteReaderAsync();
while (reader.Read())
{
results.Add(reader["Name"].ToString());
}
sqlTran.Commit();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
try
{
sqlTran.Rollback();
}
catch (Exception exRollback)
{
Debug.WriteLine(exRollback.Message);
throw;
}
throw;
}
finally
{
newInsertCmd.Connection.Close();
}
}
答案 0 :(得分:1)
The transaction operation cannot be performed because there are
pending requests working on this transaction. This SqlTransaction
has completed; it is no longer usable.
错误很可能是由于ExecuteReaderAsync
和await
造成的。尝试常规ExecuteReader
而不是await
。此应该允许.Net启动的事务工作。
如果没有解决问题,可能是您没有在reader.Close();
块中调用finally
(这应该是为了没有那个孤立的资源)。实际上,再次查看错误消息可能是在reader.Close();
之后但在while
之前需要sqlTran.Commit();
。
但是,由于您只有一个SqlCommand调用,因此确实不需要.Net启动的事务,对吧?这可以在SQL中通过如下构造来干净地处理:
BEGIN TRY
BEGIN TRANSACTION;
<your code>
COMMIT TRAN;
END TRY
BEGIN CATCH
ROLLBACK TRAN;
THROW;
END CATCH;
如果第一个INSERT(进入Parent)失败,则将跳过第二个INSERT(进入Child),因为控件将立即传递给CATCH块。
修改强>
我在IDDNReader接口的MSDN文档中遇到了以下内容,它支持我认为开放的SqlDataReader是罪魁祸首,并且在发布提交之前需要关闭。在&#34;备注&#34; Read()方法的一部分,它说:
在使用数据阅读器时,关联的连接正忙 为IDataReader提供服务。在调用Close之前就是这种情况。
这应该解释为什么&#34;关闭DataReader的先前尝试没有解决问题&#34; (在对该问题的评论中作出的陈述的释义)因为很可能你在finally
子句中关闭它,因为事务是在try
块的末尾提交的,所以为时已晚。 / p>
答案 1 :(得分:1)
这是代码,授予我当然没有经过测试甚至编译过,所以我可能会关注一些细节:
using (Context dbContext = createDbInstance())
{
//Not happy about setting MultipleActiveResultSets
string conn = dbContext.Database.Connection.ConnectionString + ";MultipleActiveResultSets=True";
using (var connection = new SqlConnection(conn))
using (var newInsertCmd = new SqlCommand(connection))
{
newInsertCmd.Connection.Open();
//Set up input variables here, including a TPV
List<string> results = new List<string>();
using(SqlTransaction sqlTran = newInsertCmd.Connection.BeginTransaction())
{
newInsertCmd.Transaction = sqlTran;
try
{
//The two insert statements work just fine. The other junk here (including the OUTPUT clause) is brand new
const string qryInsertTrans =
@"INSERT INTO Parent ([Name], [CreateDate])
SELECT n.Name, GETUTCDATE() [CreateDate]
FROM
@NewRecords n
LEFT JOIN Parent p ON n.Name = p.Name
WHERE
p.ParentID IS NULL;
DECLARE @OutputVar table(
ParentID bigint NOT NULL
);
INSERT INTO Child ([ParentID], [SomeText], [CreateDate])
OUTPUT INSERTED.ParentID INTO @OutputVar
SELECT p.ParentID, n.Text, GETUTCDATE() [CreateDate]
FROM
@NewRecords n
INNER JOIN Parent p ON n.Name = p.Name
LEFT JOIN Child c ON p.ParentID = c.ParentID AND c.SomeCol = @SomeVal
WHERE
c.ChildID IS NULL;
SELECT p.Name
FROM Parent p INNER JOIN @OutputVar o ON p.ParentID = o.ParentID";
newInsertCmd.CommandText = qryInsertTrans;
using(var reader = await newInsertCmd.ExecuteReaderAsync())
{
while (reader.Read())
{
results.Add(reader["Name"].ToString());
}
}
sqlTran.Commit();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
try
{
sqlTran.Rollback();
}
catch (Exception exRollback)
{
Debug.WriteLine(exRollback.Message);
throw;
}
throw;
}
}
}
或者,如果你正在寻找更多的东西,至少在我看来是可读的:
using (Context dbContext = createDbInstance())
{
List<string> results = new List<string>();
//Not happy about setting MultipleActiveResultSets
string conn = dbContext.Database.Connection.ConnectionString + ";MultipleActiveResultSets=True";
using (var connection = new SqlConnection(conn))
{
newInsertCmd.Connection.Open();
using(SqlTransaction sqlTran = newInsertCmd.Connection.BeginTransaction())
{
try
{
using (var parentInsert = new SqlCommand(connection))
{
parentInsert .Transaction = sqlTran;
//Set up input variables here, including a TPV
newInsertCmd.CommandText =
@"INSERT INTO Parent ([Name], [CreateDate])
SELECT n.Name, GETUTCDATE() [CreateDate]
FROM @NewRecords n
LEFT JOIN Parent p ON n.Name = p.Name
WHERE p.ParentID IS NULL;";
await newInsertCmd.ExecuteNonQueryAsync();
}
using (var childInsert = new SqlCommand(connection))
{
childInsert.Transaction = sqlTran;
//Set up input variables here, including a TPV
newInsertCmd.CommandText =
@"DECLARE @OutputVar table(
ParentID bigint NOT NULL
);
INSERT INTO Child ([ParentID], [SomeText], [CreateDate])
OUTPUT INSERTED.ParentID INTO @OutputVar
SELECT p.ParentID, n.Text, GETUTCDATE() [CreateDate]
FROM NewRecords n
INNER JOIN Parent p ON n.Name = p.Name
LEFT JOIN Child c ON p.ParentID = c.ParentID AND c.SomeCol = @SomeVal
WHERE c.ChildID IS NULL;
SELECT p.Name
FROM Parent p INNER JOIN @OutputVar o ON p.ParentID = o.ParentID";
using(var reader = await childInsert.ExecuteReaderAsync())
{
while (reader.Read())
{
results.Add(reader["Name"].ToString());
}
}
}
sqlTran.Commit();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
try
{
sqlTran.Rollback();
}
catch (Exception exRollback)
{
Debug.WriteLine(exRollback.Message);
throw;
}
throw;
}
}
}
}
为了更好的衡量,嵌入式选项:
using (Context dbContext = createDbInstance())
{
//Not happy about setting MultipleActiveResultSets
string conn = dbContext.Database.Connection.ConnectionString + ";MultipleActiveResultSets=True";
using (var connection = new SqlConnection(conn))
using (var newInsertCmd = new SqlCommand(connection))
{
newInsertCmd.Connection.Open();
//Set up input variables here, including a TPV
List<string> results = new List<string>();
//The two insert statements work just fine. The other junk here (including the OUTPUT clause) is brand new
const string qryInsertTrans =
@"BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO Parent ([Name], [CreateDate])
SELECT n.Name, GETUTCDATE() [CreateDate]
FROM @NewRecords n
LEFT JOIN Parent p ON n.Name = p.Name
WHERE p.ParentID IS NULL;
DECLARE @OutputVar table(
ParentID bigint NOT NULL
);
INSERT INTO Child ([ParentID], [SomeText], [CreateDate])
OUTPUT INSERTED.ParentID INTO @OutputVar
SELECT p.ParentID, n.Text, GETUTCDATE() [CreateDate]
FROM @NewRecords n
INNER JOIN Parent p ON n.Name = p.Name
LEFT JOIN Child c ON p.ParentID = c.ParentID AND c.SomeCol = @SomeVal
WHERE c.ChildID IS NULL;
SELECT p.Name
FROM Parent p INNER JOIN @OutputVar o ON p.ParentID = o.ParentID
COMMIT TRAN;
END TRY
BEGIN CATCH
ROLLBACK TRAN;
THROW;
END CATCH;";
newInsertCmd.CommandText = qryInsertTrans;
using(var reader = await newInsertCmd.ExecuteReaderAsync())
{
while (reader.Read())
{
results.Add(reader["Name"].ToString());
}
}
}
}