記錄一次BUG修複-Entity Framwork SaveChanges()失效

来源:https://www.cnblogs.com/InCerry/archive/2018/07/30/9390171.html
-Advertisement-
Play Games

[TOC] 一、 前言 這是筆者在參與一個小型項目開發時所遇到的一個BUG,因為項目經驗不足對Entity Framwork框架認識不足導致了這一BUG浪費了一天的時間,特此在這裡記錄。給自己一個警醒希望大家遇到相同問題能幫助到大家。 註:筆者水平有限,大家發現錯誤望批評指正。 二、問題背景 1.本 ...


目錄


一、 前言

這是筆者在參與一個小型項目開發時所遇到的一個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
QQ圖片20180124152333.png-4.3kB
1.因為引入了非同步編程的方式,開始將上文中UI層的所有非同步查詢和修改數據都改為了同步方法。

// 從資料庫查找記錄
var user = OpContext.Service.User.Where(u =>u.Id==OpContext.UserEntity.Id).FirstOrDefault();
...
if(OpContext.Service.SaveChanges() < 1)
...

QQ圖片20180124152457.png-22.7kB
QQ截圖20180124152729.png-6.7kB

更改以後通過斷電可以發現,數據正常提交至伺服器,進入修改密碼保存流程;但沒有效果,問題依舊,便開始查找更深層次的原因。

2.在其它地方添加了斷點,進行了第二次重試。有趣的事情發生了。
密碼admin居然登錄不上去了,而使用上一輪修改的1234567可以正常登錄。於是經接著提交了第二次表單。
QQ圖片20180124152715.png-28.1kB

由上圖可以看出,在記憶體中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.但是問題還是沒有解決,於是繼續斷點調試,在檢查這兩個斷點時發現了更有趣的現象。
QQ圖片20180124160227.png-28.3kB
在第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??
image_1c4jl21tk106vmk8102i1a14em54c.png-12.6kB

這個錯誤我知道,是在當前程式空間內,有一個實體對象存在於多個Entity數據上下文中,所以觸發了該錯誤,上文中將DbContext變為線程唯一就是為瞭解決這個錯誤;現在這個錯誤很明顯就是唯一性出問題了。而這是我將方法改為非同步形式後出現的,所以有以下原因。

首先得理解非同步中的await關鍵字,假設當前主線程運行,遇到await關鍵字,然後主線程就返回了。await關鍵字以下的代碼由非同步操作完成的其它線程繼續執行。

說明白點,就是下圖中178行和187行的代碼不是同一個線程執行的,所以通過EFFactory.GetDBContext()方法創建了多個DbContext對象,造成了這一問題。
image_1c4jlgvquuuvrm2e41ra81rgc4p.png-97.4kB

解決這個問題很簡單,既然一個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技術,希望能有時間。

註:筆者水平有限,大家發現錯誤望批評指正。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 程式員開發經常涉及到的記憶體區域:堆、棧、靜態存儲區域。 值類型和引用類型的區別: 本章節,暫時只介紹第1個區別:值類型和引用類型在記憶體上的存儲區域。 常用值類型:int、double、bool、char、decimal、struct、enum 常用引用類型:string、自定義類、數組 1.值類型存 ...
  • 記錄一下,方便以後用。。。感覺計算這些亂七八糟的還是有點難的,,,也許是自己還不太熟。。 ...
  • Topshelf 學習 跨平臺 Topshelf是一個開源的跨平臺的宿主服務框架,支持Windows和Mono,只需要幾行代碼就可以構建一個很方便使用的服務宿主。 官網:http://topshelf-project.com GitHub:http://github.com/topshelf/Top ...
  • 使用Common.Logging+log4net規範日誌管理 Common.Logging+(log4net/NLog/) common logging是一個通用日誌介面,log4net是一個強大的具體實現,也可以用其它不同的實現,如EntLib的日誌、NLog等。 Common.Logging可以 ...
  • 最近在做mvc跨控制器傳值的時候發現一個問題,就是有時候TempData的值為null,然後查閱了許多資料,發現了許多都是邏輯什麼的,但是真正解決的辦法什麼的都沒有案例, 於是就把自己的代碼當成案例給貼出來,方便更直觀的解決問題。 因為TempData生命周期確實很短,所以需要持久化一下: 在當前A ...
  • 1、介紹 Logging組件是微軟實現的日誌記錄組件包括控制台(Console)、調試(Debug)、事件日誌(EventLog)和TraceSource,但是沒有實現最常用用的文件記錄日誌功能(可以用其他第三方的如NLog、Log4Net。之前寫過NLog使用的文章)。 2、預設配置 新建.Net ...
  • [TOC] 一、前言 本教程是入門基礎教程,主要是筆者在項目中使用MongoDB .Net官方驅動對MongoDB內嵌文檔的操作時遇到了很多不方便的情況,踩了很多的坑,所以單獨整理出來一篇文章,來講一講筆者踩坑的過程。 筆者水平有限,如有錯誤還請批評指正! (一) 運行環境 .net版本 .Net ...
  • [TOC] 因項目需要,對於部分控制器需要實現偽靜態方便搜索引擎優化(SEO),過程比較曲折,簡單的記錄一下。 1.什麼是偽靜態?為什麼要實現偽靜態? 偽靜態:動態網頁通過重寫URL的方法實現去掉動態網頁的參數,但在實際的網頁目錄中並沒有必要實現存在重寫的頁面。 例如:我們當訪問地址http://w ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...