Linq:在TransactionScope内删除和插入相同的主键值

15

我希望在一个事务中用新记录替换数据库中的现有记录。使用TransactionScope,我已经

using ( var scope = new TransactionScope())
{
     db.Tasks.DeleteAllOnSubmit(oldTasks);
     db.Tasks.SubmitChanges();

     db.Tasks.InsertAllOnSubmit(newTasks);
     db.Tasks.SubmitChanges();

     scope.Complete();
}

我的程序抛出了

System.InvalidOperationException: Cannot add an entity that already exists.

经过一些尝试,我发现罪魁祸首在于删除和插入之间没有其他执行指令。如果在第一个SubmitChanges()和InsertAllOnSubmit()之间插入其他代码,一切都能正常工作。有人能解释为什么会这样吗?这非常令人担忧。
我尝试了另一个更新对象的方法:
IEnumerable<Task> tasks = ( ... some long query that involves multi tables )
.AsEnumerable()
.Select( i => 
{
    i.Task.Duration += i.LastLegDuration;
    return i.Task;
}
db.SubmitChanges();

这也没有起作用。数据库没有捕捉到任务的任何更改。

编辑:

这种行为似乎与事务无关。最终,我采用了极其低效的更新方法:

newTasks.ForEach( t =>
{
     Task attached = db.Tasks.Single( i => ... use primary id to look up ... );
     attached.Duration = ...;
     ... more updates, Property by Property ...
}
db.SubmitChanges();

你不能在一个事务中做这样的事情。 - Alan Turing
3
TransactionScope是BEGIN TRANSACTION和END TRANSACTION的一个包装器,因此在提交更改时,并不会真正应用更改,直到关闭新创建的事务,因此并不会真正从任务中删除任务项,但某些任务已经存在于旧和新任务列表中,因此您正在尝试将该实体添加两次,这仍然存在于列表中。 - Alan Turing
8
你是在删除一个记录,然后插入一个具有相同键的新记录吗?为什么不直接更新它呢? - Christophe Geers
除非进行Identity Insert,否则不能使用相同的键插入,EF中从未使用过该方法,但可以在SQL中使用。如果需要使用相同的键插入,则似乎只能进行更新操作。 - AD.Net
我需要更新许多角色,每个角色都有不同的值。您会推荐一种有效的方法来使用newTasks中的信息更新oldTasks中的所有信息吗? - Candy Chiu
显示剩余3条评论
2个回答

1

不要插入和删除或进行多个查询,可以尝试选择要更新的ID列表,并检查列表是否包含每个项目,以一次性更新多个行。

此外,请确保将事务标记为已完成,以指示事务管理器跨所有资源的状态是一致的,并且可以提交事务。

Dictionary<int,int> taskIdsWithDuration = getIdsOfTasksToUpdate(); //fetch a dictionary keyed on id's from your long query and values storing the corresponding *LastLegDuration*
using (var scope = new TransactionScope(TransactionScopeOption.Required))
{
    var tasksToUpdate = db.Tasks.Where(x => taskIdsWithDuration.Keys.Contains(x.id));
    foreach (var task in tasksToUpdate)
    {
        task.duration1 += taskIdsWithDuration[task.id];
    }        

    db.SaveChanges();
    scope.Complete();
}         

根据您的情况,如果您的表非常大且要更新的项目数量相对较小,则可以反转搜索以利用索引。如果是这种情况,您现有的更新查询应该可以正常工作,因此我怀疑您不需要反转它。


这个引用中有一个拼写错误。任务中并没有包含LastLegDuration,它是一个计算出来的值。 - Candy Chiu
糟糕!我现在已经修改了它,假设getIdsOfTasksToUpdate将返回一个以任务的id为键,LastLegDuration为值的Dictionary<int,int>。对于您现有的查询来说,这应该是一个简单的更改。 - arviman
你的方法看起来非常类似于逐行更新的方法。我认为大部分时间都花在了更新上,而不是获取数据。 - Candy Chiu
它类似,但您的查询将比当前的查询运行速度快得多。在您的情况下,每次更新一行时都会进行一次数据库调用。这只会进行一次调用。 - arviman
即使SubmitChange在循环外面,也会执行吗?是什么导致我的代码多次调用db? - Candy Chiu
抱歉造成误解,我的意思是当您使用where contains时,生成的SQL语句形式为SELECT * FROM [Task] Where[Task].[Id] IN (id1,id2),而不是多个SELECT子句的形式:SELECT * from [TASK] Where [Task].[Id] = id1;SELECT * from [TASK] Where [Task].[Id] = id2,这是使用foreachsingle时发生的情况。希望这能澄清一些问题。我并没有指多个网络调用 - arviman

0

我在LinqToSql中遇到了同样的问题,我认为这与事务无关,而是与会话/上下文如何合并更改有关。我这么说是因为我通过绕过linqtosql进行删除并使用一些原始的SQL来完成它来解决了这个问题。我知道这很丑陋,但它起作用了,并且都在一个事务范围内。


很遗憾,现在无法访问。不过描述起来并不难,我只是查找了如何直接在LinqtoSql中执行SQL命令,并在事务范围内使用这些命令进行删除(显然,这些命令会直接命中服务器而不需要提交更改)。我相当确定这就是我解决问题的方法,但那是一年前的事情了……upsert非常棘手,特别是当我正在编写一个从Web服务缓存数据的程序时,我不想为每个项目手动实现合并逻辑,删除/替换更容易——尤其是对于这些多表对象。 - DanH
你是否使用SQL进行了删除和插入操作? - Candy Chiu
不需要进行删除操作,我们只是欺骗状态机,这样当它将其合并回数据库时,它不会认为您正在尝试在同一键上进行删除和插入。 - DanH

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接