如何在无法插入后处理唯一约束异常以更新行?

时间:2014-07-14 22:11:21

标签: sql-server entity-framework exception-handling unique-constraint

我正在尝试处理我的Entity Framework应用程序的近同步输入。会员(用户)可以对事情进行评级,因此我有一个评分表,其中一列是会员的ID,一个是他们评价的东西的ID,一个是评级,另一个是是他们评价的时间。最近的评级应该超越之前的评级。当我收到输入时,我会检查该成员是否已经评价了一个东西,如果有,我只是使用现有行更新评级,或者如果他们没有,我会添加一个新行。我注意到,当输入来自同一个用户的同一个项目几乎同时进行时,我最终为该用户提供了两个评级相同的东西。

之前我问过这个问题:How can I avoid duplicate rows from near-simultaneous SQL adds?我按照这个建议添加了一个SQL约束,要求使用MemberID和ThingID的唯一组合,这是有道理的,但是我很难让这种技术起作用,可能是因为我我不知道在发生异常时做我想做的事情的语法。出现了一个例外,说违反了约束,我想要做的就是忘记使用相同的MemberID和ThingID非法添加一行,而是获取现有的一个并简单地将值设置为稍近一点数据。但是我还没有想出一个能够做到这一点的语法。我尝试了一些事情,当我在获得异常后尝试使用SaveChanges时,我总是遇到异常 - 要么是唯一的约束仍然出现,要么我得到死锁异常。

我尝试的最新版本是这样的:

// Get the member's rating for the thing, or create it.
Member_Thing_Rating memPref = (from mip in _myEntities.Member_Thing_Rating
                               where mip.thingID == thingId
                               where mip.MemberID == memberId
                               select mip).FirstOrDefault();
bool RetryGet = false;
if (memPref == null)
{
    using (TransactionScope txScope = new TransactionScope())
    {
        try
        {
           memPref = new Member_Thing_Rating();
           memPref.MemberID = memberId;
           memPref.thingID = thingId;
           memPref.EffectiveDate = DateTime.Now;
           _myEntities.Member_Thing_Rating.AddObject(memPref);
           _myEntities.SaveChanges();
        }
        catch (Exception ex)
        {
            Thread.Sleep(750);
            RetryGet = true;
        }
    }
    if (RetryGet == true)
    {
        Member_Thing_Rating memPref = (from mip in _myEntities.Member_Thing_Rating
                                       where mip.thingID == thingId
                                       where mip.MemberID == memberId
                                       select mip).FirstOrDefault();
    }
}

在编写完上述内容后,我还尝试在函数调用中包装逻辑,因为在离开提交更改的范围时,Entity Framework似乎会清理数据库事务。因此,我没有使用TransactionScope并在与上述相同的级别管理异常,而是将整个内容包装在一个管理函数中,如下所示:

bool Succeeded = false;
while (Succeeded == false)
{
    Thread.Sleep(750);
    Exception Problem = AttemptToSaveMemberIngredientPreference(memberId, ingredientId, rating);
    if (Problem == null)
        Succeeded = true;
    else
    {
        Exception BaseEx = Problem.GetBaseException();
    }
}

但这只会导致唯一约束上的无限串异常,在更高级别的函数中永远处理。我在尝试之间有3/4秒的延迟,所以我很惊讶可能存在报告的冲突,但当我查询一行时仍然没有找到任何结果。我想这表明所有线程都失败了,因为它们同时运行,并且实体框架注意到它们全部并且在成功之前全部失败。所以我想应该有办法通过查看所有提交并调整它们来回应异常?我不知道或看到它的语法。再说一次,处理这个问题的方法是什么?

更新: 帕迪在下面提出了三条好建议。我希望他的存储过程技术可以解决这个问题,但我仍然对这个问题的答案感兴趣。也就是说,当然应该能够通过操作提交来响应此异常,但我还没有找到语法来让它插入一行并使用最新的值。

1 个答案:

答案 0 :(得分:1)

引用Eric Lippert,"如果它疼,请停止这样做"。如果您希望获得非常高的音量,并且想要进行“插入或更新”,那么您可能需要考虑在存储过程中处理此问题而不是使用上述方法。

您的问题即将发生,因为您要求数据库检查存在与插入/更新之间存在小差距。

sproc可以使用MERGE在表格上单次传递进行插入或更新,保证您只会看到一行评级,并且它将是您收到的最新更新。


注意 - 您可以在您的EF模型中包含sproc并使用类似的EF语法调用它。


注意2 - 查看代码,在异常情况下,在休眠线程之前,不要回滚事务范围。这是一个相对较长的时间来保持交易开放,特别是当您期望非常高的交易量时。您可能希望更新代码如下:

    try
    {
       memPref = new Member_Thing_Rating();
       memPref.MemberID = memberId;
       memPref.thingID = thingId;
       memPref.EffectiveDate = DateTime.Now;
       _myEntities.Member_Thing_Rating.AddObject(memPref);
       _myEntities.SaveChanges();
       txScope.Complete();
    }
    catch (Exception ex)
    {
        txScope.Dispose();
        Thread.Sleep(750);
        RetryGet = true;
    }

这可能就是您重试时似乎遇到死锁的原因,尤其是当您收到快速并发请求时。