記錄一次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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...