记录一次BUG修复-Entity Framwork SaveChanges()失效

目录


一、 前言

这是笔者在参与一个小型项目开发时所遇到的一个BUG,因为项目经验不足对Entity Framwork框架认识不足导致了这一BUG浪费了一天的时间,特此在这里记录。给自己一个警醒希望大家遇到相同问题能帮助到大家。

注:笔者水平有限,大家发现错误望批评指正。

二、问题背景

1.本次项目是一个ASP.NET MVC项目,因为项目比较小的关系,我们采用的是基本三层和仓储模式进行开发。
2.使用的ORM框架是Entity Framwork 6.0,对其进行了封装,形成Repository层,负责对数据库进行增删改查操作。
3.项目较小和层次不多的原因,我们使用Spring.net IOC容器对每层之间的调用进行DI解耦和。
4.整个框架是从一个其它项目中搬过来的,迁移花了半天之后直接就开始实际的项目开发。
5.原有框架对Entity Framwork封装采用的都是同步方式,这里我们试水异步,项目中出现很多await/async的访问。

三、问题描述

1.因项目较小,在开发过程中后端先行,前端还没有仔细测试。这是后端开发基本完成以后,加入前端测试时出现的问题。
2.前端测试过程中,可以增加、删除数据但无法保存修改的数据

贴出关键代码

以下是UI层代码,其作用是更改用户的当前密码。

[HttpPost]
public async Task<ActionResult> ChangePassword(ChangePasswordViewModel changePasswordViewModel)
{
    // 检查模型
    if (ModelState.IsValid == false)
    {
        return OpContext.JsonMsgFail(MODEL_VALIDATE_ERROR);
    }

    // 检查验证码
    if (OpContext.CheckValidateCode(changePasswordViewModel.validateCode) == false)
    {
        return OpContext.JsonMsgFail(MODEL_VALIDATECODE_ERROR);
    }

    // 从数据库查找记录
    var user = await OpContext.Service.User
    .Where(u =>u.Id==OpContext.UserEntity.Id).FirstOrDefaultAsync();
    if (changePasswordViewModel.oldPassword != user.UserPassword)
    {
        return OpContext.JsonMsgFail(CHECK_PASSWORD_ERROR);
    }

    // 更改密码并保存更改
    user.UserPassword = changePasswordViewModel.newPassword;
    try
    {
        OpContext.Service.User.Modify(user, new string[]{ "UserPassword" });
        if(await OpContext.Service.SaveChangesAsync() < 1)
            return OpContext.JsonMsgErr(DATA_SAVECHANGES_ERROR);
    }
    catch (Exception ex)
    {
        return OpContext.JsonMsgErr(ex.Message);
    }

    return OpContext.JsonMsgOK(DATA_MODIFY_SUCCESS);
}

以下是Repository层代码,关键是获取DbContext对象和更改实体的代码。

protected EntitiesContainer DbContext { get; private set; } = EFFactory.GetDBContext();

......

/// <summary>
/// 修改实体
/// </summary>
/// <param name="model">模型</param>
/// <returns></returns>
public void Modify(T model)
{
    DbContext.Entry<T>(model).State = System.Data.Entity.EntityState.Modified;
}

/// <summary>
/// 修改实体
/// </summary>
/// <param name="model">模型</param>
/// <param name="modifyPropertyNames">修改的属性名</param>
/// <returns></returns>
public void Modify(T model,params string[] modifyPropertyNames)
{
    var entry = DbContext.Entry<T>(model);
    entry.State = System.Data.Entity.EntityState.Unchanged;
    foreach(var pName in modifyPropertyNamesValues)
    {
        entry.Property(pName).IsModified = true;
    }
}

/// <summary>
/// 修改指定实体
/// </summary>
/// <param name="whereLamdba">修改条件</param>
/// <param name="modifyPropertyNamesValues">修改属性和值</param>
/// <returns></returns>
public void ModifyBy(Expression<Func<T, bool>> whereLamdba, Dictionary<string, object> modifyPropertyNamesValues)
{
    var models = DbContext.Set<T>().Where(whereLamdba);
    Type t = typeof(T);

    foreach (var model in models)
    {
        foreach (var pNameValue in modifyPropertyNamesValues)
        {
            PropertyInfo pi = t.GetProperty(pNameValue.Key);
            pi.SetValue(model, pNameValue.Value);
        }
    }
}

EF工厂从当前线程上下文获取数据库上下文。

public static class EFFactory
{
    /// <summary>
    /// 从线程上下文中获取EF容器
    /// </summary>
    /// <returns></returns>
    public static EntitiesContainer GetDBContext()
    {
        var context = CallContext.GetData(nameof(EntitiesContainer));

        if (context == null)
        {
            context = new EntitiesContainer();
            CallContext.SetData(nameof(EntitiesContainer), context);
        }

        return context as EntitiesContainer;
    }
}

四、问题解决步骤

以上一节中的代码是有问题的源代码,因为该项目框架是从别的正常项目中移植过来,所以开始并没有怀疑代码的正确性,从客户端代码入手。

提交的表单数据如下,原始密码为:admin,需修改为1234567

1.因为引入了异步编程的方式,开始将上文中UI层的所有异步查询和修改数据都改为了同步方法。

// 从数据库查找记录
var user = OpContext.Service.User.Where(u =>u.Id==OpContext.UserEntity.Id).FirstOrDefault();
...
if(OpContext.Service.SaveChanges() < 1)
...


更改以后通过断电可以发现,数据正常提交至服务器,进入修改密码保存流程;但没有效果,问题依旧,便开始查找更深层次的原因。

2.在其它地方添加了断点,进行了第二次重试。有趣的事情发生了。
密码admin居然登录不上去了,而使用上一轮修改的1234567可以正常登录。于是经接着提交了第二次表单。

由上图可以看出,在内存中user.UserPassword已经变更为1234567但是数据库中任然没有反应。这是为什么?聪明的大伙说不定已经猜出原因了。

笔者看到这个情况估计是Entity Framwork的数据缓存机制的原因,在上一次的修改中数据在内存中已经被修改,但是由于其它原因没有写入数据库。所以造成了第二次登录时直接使用的缓存中的数据。

由上可得以下分析:

(1).大家都知道,在项目中一些常用的工具类可以编写成静态类的方式节省时间和内存,其它不能编写为静态类的可通过单例模式来让整个程序运行空间只有一个实例。
(2).所以项目中的Repository层其实都是单例模式,节省new的时间和内存开支。而我们的DbContext数据上下文因为EF会追踪所有实体如果使用单例的话会疯狂吃内存,而且可能会发生“脏读”现象,所以一般都把它做成线程内唯一,也是笔者这个项目的做法。
(3).所以按照正常逻辑一个HTTP请求对应一个处理线程和一个DbContext对象,不可能发生第二次请求会使用第一次的缓存的现象,绝对是线程唯一出现了问题。

3.于是查看代码,发现了这一条语句。

protected EntitiesContainer DbContext { get; private set; } = EFFactory.GetDBContext();

这一条语句在笔者在设计文档中查看其作用是:“每次访问DbContext对象都调用EFFactory.GetDBContext()方法,从而从当前线程中读取线程惟一的DbContext对象。”
相当于以下代码。

protected EntitiesContainer DbContext()
{
    return EFFactory.GetDBContext();
}

但是现在这一条语句的作用却相当于这段代码,也就是说只会初始化一次。

private EntitiesContainer dbContext = EFFactory.GetDBContext();
protected EntitiesContainer DbContext()
{
    return dbContext;
}

然后将其改为设计中等价的代码,发现缓存的问题就不存在了,但是仍然不能保存更改。
一不小心揪出了一个存在项目中4年的BUG,好兴奋。

protected HynuIOTAEntitiesContainer DbContext => EFFactory.GetDBContext();
    return EFFactory.GetDBContext();
}

但是现在这一条语句的作用却相当于这段代码,也就是说只会初始化一次。

private EntitiesContainer dbContext = EFFactory.GetDBContext();
protected EntitiesContainer DbContext()
{
    return dbContext;
}

然后将其改为设计中等价的代码,发现缓存的问题就不存在了,但是仍然不能保存更改。
一不小心揪出了一个存在项目中4年的BUG,好兴奋。

protected HynuIOTAEntitiesContainer DbContext => EFFactory.GetDBContext();
    那为什么老项目用的好好的,没有问题呢?因为笔者在开头提过,为了节省时间和内存,将Repository层被设置成单例层,所以才造成这一问题,老项目中每次使用Respository都是重新new,所并不存在问题。

3.但是问题还是没有解决,于是继续断点调试,在检查这两个断点时发现了更有趣的现象。

在第77行的时候我检查model其中UserPassword属性已经被改为"1234567",但是到第79,神奇的UserPas
sword属性又变为了"admin",给还原了。 WHY???????
于是笔者查看了老项目中的代码,是一个更新服务器列表的操作,代码如下。

var serverState = OpContext.Service.ServerState
    .Where(s => s.Id == Server.MachineId).FirstOrDefault();
if(Server.IsConnect == false)
{
    serverState.IsConnect = false;
    result = OpContext.Service.SaveChanges();
}

老项目中的代码完全没有执行Modify操作,难道不需要Modify就可以直接保存么?
于是笔者将Modify操作的代码删除以后,更改正常同步进入了数据库中。
查询了相关文档,发现了重点的几句话。

Entity Framwork ChangeTracker会跟踪数据上下文实体的更改状况,只有当数据上下文中不存在其实体,才会使用Modify将更改添加至数据上下文,进行更改操作。

知识点:
也就是说在之前使用OpContext.Service.User.Where(u=>u.Id==OpContext.UserEntity.Id).FirstOrDefault()已经将数据查询出来,数据上下文中已经存在实体对象,ChangeTracker会跟踪其更改状态,不用多此一举的使用Modify方法,直接SaveChange就可以。

问题就这么解决了么?目前是的,所有功能都正常,可以正常更改并保存至数据库中。
于是我又愉快的把代码改回异步形式,重新测试了一遍。
Excuse me??

这个错误我知道,是在当前程序空间内,有一个实体对象存在于多个Entity数据上下文中,所以触发了该错误,上文中将DbContext变为线程唯一就是为了解决这个错误;现在这个错误很明显就是唯一性出问题了。而这是我将方法改为异步形式后出现的,所以有以下原因。

首先得理解异步中的await关键字,假设当前主线程运行,遇到await关键字,然后主线程就返回了。await关键字以下的代码由异步操作完成的其它线程继续执行。

说明白点,就是下图中178行和187行的代码不是同一个线程执行的,所以通过EFFactory.GetDBContext()方法创建了多个DbContext对象,造成了这一问题。

解决这个问题很简单,既然一个HTTP请求对应多个线程,线程唯一对象没办法满足要求,那么我们使用HTTP请求内唯一的方法改造GetDBContext()。

public static EntitiesContainer GetDBContext()
{
    var context = HttpContext.Current.Items[nameof(EntitiesContainer)] as EntitiesContainer;
    if (context == null)
    {
        context = new EntitiesContainer();
        HttpContext.Current.Items[nameof(EntitiesContainer)] = context;
    }
    return context as EntitiesContainer;
}

这样就实现了一个HTTP请求对应一个DbContext对象

六、总结

在本次BUG的查找和修复过程中,感触良多。因为对Entity Framwork框架的不熟悉,走了很多弯路。这一次BUG的出现让我很大的理解了Entity Framwork数据缓存和ChangeTracker技术,打算近段时间出一个专栏,详细了解一下Entity Framwork技术,希望能有时间。

https://www.cnblogs.com/InCerry/p/9390171.html

原文地址:https://www.cnblogs.com/sjqq/p/9460238.html

时间: 2024-11-05 15:00:47

记录一次BUG修复-Entity Framwork SaveChanges()失效的相关文章

1.使用Entity Framwork框架常用的技术手段Code First 和Reverse Engineer Code First

提示:VS版本2013,  Entity Framwork版本5.0.0,Mysql数据库  使用Entity FrameWork的好处就不多说,直接上手如何使用.两种形式:1.将代码映射到数据库实体,使用的是Code First技术.2.将数据库实体转成代码,使用的是Reverse Engineer Code Fist技术. 一.Code First技术 1.建好项目,并加入一个名为CodeFirstDemo控制台应用程序项目. 2.配置.工具>>库程序包管理>>管理解决方案的N

[bug修复方案分享]阿拉伯文导致TextView显示顺序左右颠倒

bug现象: 在“?(·ω·) ?”这个表情后通过StringBuilder拼接的文字会显示在表情内部,例如拼接“2015”后TextView控件中显示为“?(·ω·) ?2015” bug原因排查: Step1:怀疑是工程师调用append方法错误,但通过debug调试查看String的char[]值正常 Step2:怀疑是“ ?”这个特殊字符引起的问题,删除后显示正常 Step3:百度搜索“ ?”字符,发现是阿拉伯语文字,结合阿拉伯语从右至左的阅读.书写顺序,确认bug原因 bug修复方法:

OJ2.0userInfo页面Modify逻辑bug修复,search功能逻辑实现

这周的主要任务:userInfo页面Modify逻辑bug修复,search功能逻辑实现. (一)Modify逻辑bug修复: 这里存在的bug就是在我们不重置密码的时候按照前面的逻辑是不能提交修改,这个逻辑是错误的,应该改为可以不修改密码也能提交,主要是if逻辑判断的修改 先看一下代码: def userInfo(request, user_id): try: user = User.objects.get(userID = request.session['userID']) except:

NHibernate 的 SetResultTransformer 方法在Oracle下的Bug修复

NHibernate 的 SetResultTransformer 方法在Oracle下会出现"Could not find a setter for property"错误,这是Nhibernate在Oracle下使用的一个Bug.针对此Bug我可以自己进行修复. 下载NHibernate源码,将Property下的"ChainedPropertyAccessor.cs"稍作修改就会修复此Bug,代码如下: using System; namespace NHib

android-misc-widgets四向(上下左右)抽屉bug修复版--转载

 android-misc-widgets四向(上下左右)抽屉bug修复版 2013-08-04 08:58:13 标签:bug down top panel slidingdrawer 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://mikewang.blog.51cto.com/3826268/1263802 一,与开源项目相比,改进如下: 1,修复了闪屏的问题 二,与开源项目相比,增加的限制: 1,不能设置相应动画的In

记录android的bug

java.lang.IllegalStateException: Target host must not be null, or set in parameters. 原因:连接地址不完整,必须加上"http://". =============================================================== java.net.UnknownHostException: Host is unresolved: www.baidu.com在andro

duilib BUG修复 --- 按一次ESC键, 关闭多个窗口

BUG造成的影响 继承自WindowImplBase的窗口类, 如果没有重写重写ResponseDefaultKeyEvent而由WindowImplBase默认处理的话, 会导致按一次有ESC键, 导致多个窗口全部关闭. 产生原因 第1张图: ESC按键属于PreMessage, 所以会经由这里的PreMessageHandlers处理, 如果 MessageHandler 处理了该消息, 那么她就应该置 bHandled 为 true, 以防止 ESC 消息继续往下传导致多个窗口关闭. 第2

bootstrap关于日期控件被模态框遮盖的BUG修复

bootstrap关于日期控件被模态框遮盖的BUG修复 在使用bootstrap-datepicker.js插件的时候,由于是在模态框中填写日期值,而模态框的值把日期控件遮盖在下面了,导致无法填写日期值.如下图所示: 其本质原因是:当我们点击日期控件填写日期时,bootstrap-datepicker.js帮我们动态生成了class为datepicker dropdown-menu的div,其中包括日期控件中的由年月日等构成的div,而dropdown-menu样式是在bootstrap.css

MDT 2013 Update 1 Preview 部署 Windows 10之MDT部署BUG修复

本章主要内容是修复MDT 2013Update 1 preview 相关配置Bug,从而为部署windows 10提供配置支持,可能相对于之前自动化设置较为复杂,建议大家在修改过程中做好原目录下数据的备份,以防止配置错误后可实时恢复.有关本文中所涉及到的替换配置文件请参照附件中内容.接下来我们开始进入正题: 1.打开存放MDT 2013Update 1 Bug修复文件夹: 2.复制Client.xml和StateRestore.xml两文件,并覆盖MDT安装目录C:\Program Files\