一、 前言

这是笔者在参与一个小型项目开发时所遇到的一个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. js模态框实现原理

    <!DOCTYPE> <html> <head> <style>/* 定义模态对话框外面的覆盖层样式 */ #modal-overlay { visib ...

  2. activiti如何获取当前节点以及下一步路径或节点(转)

    ACTIVITI相对于JBPM来说,比较年轻,用的人少,中文方面的资料更少,我根据网上到处找得资料以及看官方文档总结出来了代码,非常不容易啊.废话不多说,直接上代码吧: 首先是根据流程ID获取当前任务 ...

  3. HACK字体安装

    参考:https://github.com/source-foundry/Hack Linux的 下载最新版本的Hack. 从存档中提取文件(.zip). 将字体文件复制到系统字体文件夹(通常/usr ...

  4. SQL 创建联合主键Table

    CREATE TABLE [User_Instance]( [IntanceID] [int] NOT NULL, ) NOT NULL ) ON [PRIMARY] GO SET ANSI_PADD ...

  5. 使用policheck 检测

    Policheck is a profing and testing tool for sensitive terminology and helps in ensuring thattrustwor ...

  6. HYSBZ - 3676

    模板题.问你一个串里最大的值(回文子串*出现次数) /* gyt Live up to every day */ #include<cstdio> #include<cmath> ...

  7. CAS 单点登录 服务器整合

    概述 现在企业内部的系统越来越多,如果各个应用都有自己的用户系统,那么用户将不得不要记住不同系统的用户名密码,因此独立的用户系统应运而生,各个系统之间通过单点登录的方式,这样内部只需要记住一个用户名和 ...

  8. ZOJ 3156 Taxi (二分 + 二分匹配)

    题意:给定 n 个人坐标, m 辆车的坐标,还有人的速度,要求每个人要进一辆不同的车,问你所有都进车的最短时间是多少. 析:首先二分时间 mid,很明显就是最后那个人进车的时间,然后如果把第 i 个人 ...

  9. CodeForces 916B Jamie and Binary Sequence (changed after round) (贪心)

    题意:给定两个数字n,m,让你把数字 n 拆成一个长度为 m 的序列a1,a2,a3...am,并且∑2^ai = n,如果有多组,要求序列中最大的数最小,然后再相同就要求除了最大数字典序最大. 析: ...

  10. php,ajax上传文件,多文件上传

    HTML <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF- ...