一、 前言

这是笔者在参与一个小型项目开发时所遇到的一个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技术,希望能有时间。

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

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

  1. ThinkPHP 3.2.3+ORACLE插入数据BUG修复及支持获取自增Id的上次记录

    TP+ORACLE插入数据BUG修复以及获取自增Id支持getLastInsID方法 这些天在做Api接口时候,发现用TP操作Oracle数据库,发现查询修改删除都能执行, 但一旦执行插入操作老是报错 ...

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

    提示:VS版本2013,  Entity Framwork版本5.0.0,Mysql数据库  使用Entity FrameWork的好处就不多说,直接上手如何使用.两种形式:1.将代码映射到数据库实体 ...

  3. Spring+SpringMVC+MyBatis+easyUI整合基础篇(八)mysql中文查询bug修复

    写在前面的话 在测试搜索时出现的问题,mysql通过中文查询条件搜索不出数据,但是英文和数字可以搜索到记录,中文无返回记录.本文就是写一下发现问题的过程及解决方法.此bug在第一个项目中点这里还存在, ...

  4. Saiku Table展示数据合并bug修复(二十五)

    Saiku Table展示数据合并bug修复 Saiku以table的形式展示数据,如果点击了 非空的字段 按钮,则会自动进行数据合并,为空的数据行以及数据列都会自动隐藏掉. 首先我们应该定位问题: ...

  5. 仿酷狗音乐播放器开发日志十九——CTreeNodeUI的bug修复二(附源码)

    转载请说明原出处,谢谢 今天本来打算把仿酷狗播放列表的子控件拖动插入功能做一下,但是仔细使用播放列表控件时发现了几个逻辑错误,由于我的播放 列表控件是基于CTreeViewUI和CTreeNodeUI ...

  6. Entity Framwork db First 中 Model验证解决办法。

    由于项目中用到 Entity Framwork db First     每次从数据库生成数据模型之后都会把模型更新. 只要有一个表更新.所有的类都会重新生成. 在网上找了各种例子都是差不多的, 可能 ...

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

    这周的主要任务:userInfo页面Modify逻辑bug修复,search功能逻辑实现. (一)Modify逻辑bug修复: 这里存在的bug就是在我们不重置password的时候依照前面的逻辑是不 ...

  8. cocos2d-x多分辨率和随后的自适应CCListView的bug修复

    cocos2d-x多分辨率自适配及因此导致的CCListView的bug修复 cocos2d-x是一款众所周知的跨平台的游戏开发引擎.因为其跨平台的特性.多分辨率支持也自然就有其需求. 因此.在某一次 ...

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

     android-misc-widgets四向(上下左右)抽屉bug修复版 2013-08-04 08:58:13 标签:bug down top panel slidingdrawer 原创作品,允 ...

随机推荐

  1. 算法题思路总结和leecode继续历程

    2018-05-03 刷了牛客网的题目:总结思路(总的思路跟数学一样就是化简和转化) 具体启发点: 1.对数据进行预处理排序的思想:比如8皇后问题 2.对一个数组元素进行比较的操作,如果复杂,可以试试 ...

  2. Java SE学习【二】——面向对象

    面向对象的学习也进行了一段时间,这段时间学了,类和对象:属性:方法:封装:继承:多态:接口.也算是有一些自己的理解,不愧是贴近人类思维的思想,老师讲时我常常会想到以前的一些事物和其交相印证,其中最常想 ...

  3. mac安装oh my zsh

    克隆项目到本地 git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh 2.创建配置文件 cp ~/.oh-my-zsh/ ...

  4. AJAX-php-json数组

    1.在php中有个数组,响应回前端 $array=["习大大","川普","金三胖"];2.JS对象数据格式 ex: 数组: var TOM ...

  5. php结合layui实现前台加后台操作

    一:前台加载出前端页面: HTML: lay-data="{width:800,height:400, url:'data.php', page:true, id:'test'} js: l ...

  6. tms web core 里面调用pascal 过程。

    procedure show(s:string);begin  showmessage(s);end; procedure TForm3.WebButton1Click(Sender: TObject ...

  7. 取消IDEA中代码重复的检测

  8. 爬虫模块之解决IO

    一 asyncio模块 asyncio模块:主要是帮我们检测IO(只能是网路IO). @asyncio.coroutine:装饰器 tasks:任务列表 get_event_loop:起任务 run_ ...

  9. ThinkPHP3.2 伪静态配置

    前台伪静态且隐藏入口文件 就把“.htaccess文件” 放到指定文件夹下面 如图home做伪静态并隐藏入口文件: Apache为例,需要在入口文件的同级添加.htaccess文件 如果用的phpst ...

  10. String拼接

    1. String[] members=ArrayDATA_IDS; String usernames = ""; for(String id :members) { userna ...