更新选择性字段时,EF5无法处理并发

Vip*_*esh 5 c# database asp.net-mvc concurrency entity-framework

我正在使用EF5和Data First方法来更新实体.我正在使用其他问题建议的方法来有条件地更新实体中的已修改属性.

Oki所以这里的场景是我的控制器用POCO对象调用Service并从Service获取POCO对象,Service层与Data层对话,Data层在内部使用EF5从DB检索实体并在DB中更新它们.

控制器从服务层检索的DTO对象加载View数据.用户更改View和Posts将JSON数据发送回控制器,控制器映射到控制器中的DTO对象(礼貌MVC).控制器使用DTO对象(PO​​CO)对象调用Service层.服务将POCO对象映射到EF实体对象,并调用传入EF实体的数据层(即存储库)更新方法.在Repository中,我从DB获取现有实体并调用ApplyCurrentvaluesValues方法,然后检查是否修改了任何属性.如果修改了属性,那么我将自定义逻辑应用于与当前实体无关的其他实体,并更新当前实体的"UpdatedAdminId"和"UpdationDate".发布这个我在Centext上调用"SaveChanges"方法.

我提到的每一件事都很好,除非我在"SaveChanges"调用中插入一个断点并将User修改的某些字段更新为不同的值,否则EF5不会抛出"DbUpdateConcurrencyException".即当我感兴趣的属性被修改为完美运行时,我可以获得有条件的更新并触发我的自定义逻辑.但是在并发的情况下我没有收到错误,即如果在我从DB获取记录,更新记录并保存记录之间更新记录,则EF不会引发"DbUpdateConcurrencyException".

在实际场景中,有一个离线cron运行,它检查新创建的广告系列并为它们创建投资组合,并将下面的IsPortfolioCreated属性标记为true,同时用户可以编辑广告系列,并且即使cron也可以将标志设置为false创建了投资组合.

为了复制并发场景,我在SaveChanges上设置了一个断点,然后从MS-Sql企业管理器中为同一个实体更新了IsPortfolioCreated feild,但是即使已经更新了Store中的Data,也不会抛出"DbUpdateConcurrencyException".

这是我的代码供参考,

Public bool EditGeneralSettings(CampaignDefinition campaignDefinition)
{
    var success = false;
    //campaignDefinition.UpdatedAdminId is updated in controller by retreiving it from RquestContext, so no its not comgin from client
    var updatedAdminId = campaignDefinition.UpdatedAdminId;
    var updationDate = DateTime.UtcNow;
    CmsContext context = null;
    GlobalMasterContext globalMasterContext = null;

    try
    {
        context = new CmsContext(SaveTimeout);

        var contextCampaign = context.CampaignDefinitions.Where(x => x.CampaignId == campaignDefinition.CampaignId).First();

        //Always use this fields from Server, no matter what comes from client
        campaignDefinition.CreationDate = contextCampaign.CreationDate;
        campaignDefinition.UpdatedAdminId = contextCampaign.UpdatedAdminId;
        campaignDefinition.UpdationDate = contextCampaign.UpdationDate;
        campaignDefinition.AdminId = contextCampaign.AdminId;
        campaignDefinition.AutoDecision = contextCampaign.AutoDecision;
        campaignDefinition.CampaignCode = contextCampaign.CampaignCode;
        campaignDefinition.IsPortfolioCreated = contextCampaign.IsPortfolioCreated;

        var campaignNameChanged = contextCampaign.CampaignName != campaignDefinition.CampaignName;

        // Will be used in the below if condition....
        var originalSkeForwardingDomain = contextCampaign.skeForwardingDomain.ToLower();
        var originalMgForwardingDomain = contextCampaign.mgForwardingDomain.ToLower();

        //This also not firing concurreny  exception....
        var key = ((IObjectContextAdapter) context).ObjectContext.CreateEntityKey("CampaignDefinitions", campaignDefinition);
        ((IObjectContextAdapter)context).ObjectContext.AttachTo("CampaignDefinitions", contextCampaign);
        var updated = ((IObjectContextAdapter)context).ObjectContext.ApplyCurrentValues(key.EntitySetName, campaignDefinition);
        ObjectStateEntry entry = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager.GetObjectStateEntry(updated);
        var modifiedProperties = entry.GetModifiedProperties();

        //Even tried this , works fine but no Concurrency exception
        //var entry = context.Entry(contextCampaign);
        //entry.CurrentValues.SetValues(campaignDefinition);
        //var modifiedProperties = entry.CurrentValues.PropertyNames.Where(propertyName => entry.Property(propertyName).IsModified).ToList();

        // If any fields modified then only set Updation fields
        if (modifiedProperties.Count() > 0)
        {
            campaignDefinition.UpdatedAdminId = updatedAdminId;
            campaignDefinition.UpdationDate = updationDate;
            //entry.CurrentValues.SetValues(campaignDefinition);
            updated = ((IObjectContextAdapter)context).ObjectContext.ApplyCurrentValues(key.EntitySetName, campaignDefinition);

            //Also perform some custom logic in other entities... Then call save changes

            context.SaveChanges();

            //If campaign name changed call a SP in different DB..
            if (campaignNameChanged)
            {
                globalMasterContext = new GlobalMasterContext(SaveTimeout);
                globalMasterContext.Rename_CMS_Campaign(campaignDefinition.CampaignId, updatedAdminId);
                globalMasterContext.SaveChanges();
            }
        }
        success = true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        //Code never enters here, if it does then I am planning to show the user the values from DB and ask him to retry
        //In short Store Wins Strategy

        //Code in this block is not complete so dont Stackies don't start commenting about this section and plague the question...

        // Get the current entity values and the values in the database 
        var entry = ex.Entries.Single();
        var currentValues = entry.CurrentValues;
        var databaseValues = entry.GetDatabaseValues();

        // Choose an initial set of resolved values. In this case we 
        // make the default be the values currently in the database. 
        var resolvedValues = databaseValues.Clone();


        // Update the original values with the database values and 
        // the current values with whatever the user choose. 

        entry.OriginalValues.SetValues(databaseValues);
        entry.CurrentValues.SetValues(resolvedValues);

    }
    catch (Exception ex)
    {
        if (ex.InnerException != null)
            throw ex.InnerException;
        throw;
    }
    finally
    {
        if (context != null) context.Dispose();
        if (globalMasterContext != null) globalMasterContext.Dispose();
    }
    return success;
}
Run Code Online (Sandbox Code Playgroud)

小智 2

实体框架不会对并发做任何特殊的事情,直到您(作为开发人员)配置它来检查并发问题。

您正在尝试捕获 DbUpdateConcurrencyException,此异常的文档说: “当预计实体的 SaveChanges 会导致数据库更新时,DbContext 抛出异常,但实际上数据库中没有行受到影响。”,您可以阅读这里

在数据库优先方法中,您必须将列的属性“并发模式”设置为“固定”(默认值为“无”)。看这个截图:在此输入图像描述

Version 列是 SQL SERVER TIMESTAMP 类型,这是一种特殊类型,每次行更改时都会自动更新,请在此处阅读

使用此配置,您可以尝试这个简单的测试,如果一切都按预期工作:

try
{
    using (var outerContext = new testEntities())
    {
        var outerCust1 = outerContext.Customer.FirstOrDefault(x => x.Id == 1);
        outerCust1.Description += "modified by outer context";
        using (var innerContext = new testEntities())
        {
            var innerCust1 = innerContext.Customer.FirstOrDefault(x => x.Id == 1);
            innerCust1.Description += "modified by inner context";
            innerContext.SaveChanges();
        }
        outerContext.SaveChanges();
    }
}
catch (DbUpdateConcurrencyException ext)
{
    Console.WriteLine(ext.Message);
}
Run Code Online (Sandbox Code Playgroud)

在上面的示例中,来自内部上下文的更新将被提交,来自外部上下文的更新将引发 DbUpdateConcurrencyException,因为 EF 将尝试使用 2 列作为过滤器来更新实体:Id 和 Version 列。

希望这可以帮助!