首页 > 解决方案 > 实体框架和 SQL Server 中的奇怪 SaveChanges 行为

问题描述

我有一些代码,你可以检查项目github,错误包含在UploadContoller方法中GetExtensionId

数据库图:

在此处输入图像描述

代码(在这个控制器中我发送文件上传):

    [HttpPost]
    public ActionResult UploadFiles(HttpPostedFileBase[] files, int? folderid, string description)
    {
        foreach (HttpPostedFileBase file in files)
        {
            if (file != null)
            {
                string fileName = Path.GetFileNameWithoutExtension(file.FileName);
                string fileExt = Path.GetExtension(file.FileName)?.Remove(0, 1);
                
                int? extensionid = GetExtensionId(fileExt);
                
                if (CheckFileExist(fileName, fileExt, folderid))
                {
                    fileName = fileName + $" ({DateTime.Now.ToString("dd-MM-yy HH:mm:ss")})";
                }

                File dbFile = new File();
                dbFile.folderid = folderid;
                dbFile.displayname = fileName;
                dbFile.file_extensionid = extensionid;
                dbFile.file_content = GetFileBytes(file);
                dbFile.description = description;

                db.Files.Add(dbFile);
            }
        }
        db.SaveChanges();
        return RedirectToAction("Partial_UnknownErrorToast", "Toast");
    }

如果它尚不存在,我想在数据库中创建扩展。我这样做GetExtensionId

    private static object locker = new object();
    private int? GetExtensionId(string name)
    {
        int? result = null;
        lock (locker)
        {
            var extItem = db.FileExtensions.FirstOrDefault(m => m.displayname == name);

            if (extItem != null) return extItem.file_extensionid;

            var fileExtension = new FileExtension()
            {
                displayname = name
            };
            db.FileExtensions.Add(fileExtension);
            db.SaveChanges();
            result = fileExtension.file_extensionid;
        }
        return result;
    }

在 SQL Server 数据库中,我对 FileExtension 的 displayname 列有唯一约束。

仅当我上传几个具有相同扩展名的文件并且该扩展名在数据库中尚不存在时才会出现问题。

如果我删除lock, inGetExtensionId将是Exception关于唯一约束。

也许,出于某种原因,下一次foreach循环调用迭代GetExtensionId而不等待?我不知道。但只有当我设置lock我的代码工作正常。

如果你知道为什么会发生,请解释。

标签: c#sql-serverentity-frameworkunique-constraintsavechanges

解决方案


这听起来像是一个简单的并发竞争条件。想象一下同时有两个请求进来;他们都检查了FirstOrDefault,这对两者都正确地说“不”。然后他们都尝试插入;一场胜利,一场失败,因为数据库发生了变化。虽然 EF 管理周围的事务SaveChanges,但该事务并非从您最初查询数据时开始

通过防止他们同时进入查看代码,这lock似乎有效,但这通常不是一个可靠的解决方案,因为它只能在单个进程中工作,更不用说节点了。

所以:这里有几个选项:

  • 您的代码可以检测到外键违规异常并从头开始重新检查(FirstOrDefault 等),这在成功情况下(这将是大部分时间)保持简单,并且在失败情况下不会非常昂贵(只是一个异常和额外的数据库命中) - 足够务实
  • 您可以将“如果存在则选择,如果不存在则插入”移动到事务内数据库内的单个操作中(理想情况下可序列化隔离级别和/或使用UPDLOCK提示) - 这需要自己编写 TSQL,而不是依赖EF,但最大限度地减少往返行程并避免编写“检测故障并补偿”代码
  • 您可以通过 EF 在事务中执行选择和可能的插入- 坦率地说,复杂而混乱:不要这样做(它再次需要可序列化的隔离级别,但现在可序列化的事务跨越多个往返,可以开始影响锁定,如果在规模上)

推荐阅读