別跟我談EF抵抗併發,敢問你到底會不會用EntityFramework

前言

一直以來寫的博文都是比較溫婉型的博文,今天這篇博文算是一篇批判性博文,有問題歡迎探討,如標題,你到底會不會用EntityFramework啊。面試

你到底會不會用EntityFramework啊

  面試過三年至六年的同行,做爲過面試者到現在做爲面試官也算是老大對個人信任,對來面試的面試者的任何一位同行絕沒有刁難之意,若還裝逼那就沒有什麼意義。我也基本不看面試者的項目經歷,由於我我的以爲每一個面試者所在公司所作項目都不同,可能面試者項目所作的業務我一點都不知道,而我所關心的是項目當中所用到的技術,稍微看下了簡歷讓面試者簡單作個自我介紹,這算是基本流程吧。而後直接問面試者最擅長的技術是哪些?好比ASP.NET MVC、好比ASP.NET Web APi、好比EntityFramework,再好比數據庫等等。若是面試者沒有特別擅長的技術那我就簡歷上提出所熟悉和項目當中用到的技術進行提問。這裏暫且不提其餘技術,單單說EntityFramework,面試的面試者大部分都有用過EntityFramework,我就簡單問了下,好比您用的EntityFramework版本是多少?答案是不知道,這個我理解,可能沒去關心過這個問題,再好比我問您知道EntityFramework中有哪些繼承策略,而後面試者要麼是一臉懵逼,要麼是不知道,要麼回了句咱們不用。這個我也能理解,重點來了,我問您在EntityFramwork中對於批量添加操做是怎麼作的,無一例外遍歷循環一個一個添加到上下文中去,結果令我驚呆了,或許是隻關注於實現,不少開發者只關注這個能實現就行了,這裏不過多探討這個問題,每一個人觀點不同。數據庫

  大部分人用EntityFramework時出現了問題,就吐槽EntityFramework啥玩意啊,啥ORM框架啊,各類問題,我只能說您根本不會用EntityFramework,甚至還有些人併發測試EntityFramework的性能,是的,沒錯,EntityFramework性能不咋的(這裏咱們只討論EF 6.x),或者說在您實際項目當中有了點併發發現EF出了問題,又開始抱怨EF不行了,同時對於輕量級、跨平臺、可擴展的EF Core性能秒殺EF,即便你併發測試EF Core性能也就那麼回事,我想說的是你併發測試EF根本沒有任何意義,請好生理解EF做爲ORM框架出現的意義是什麼,不就是爲了讓咱們關注業務麼,梳理好業務對象,在EF中用上下文操做對象就像直接操做表同樣。而後咱們回到EF抵抗併發的問題,有的童鞋認爲EF中給我提供了併發Token和行版本以及還有事務,這不就是爲了併發麼,童鞋對於併發Token和行版本這是對於少許的請求可能存在的併發EF團隊提出的基本解決方案,對於事務不管是同一上文抑或是跨上下文也好只是爲了保證數據一致性罷了。要是大一點的併發來了,您難道還讓EF不顧一切衝上去麼,這無疑是飛蛾撲火自取滅亡,你到底會不會用EntityFramework啊。EF做爲概念上的數據訪問層應該是處於最底層,若是咱們項目可預見沒有所謂的併發問題,將上下文直接置於最上層好比控制器中並無什麼問題,可是項目比較大,隨着用戶量的增長,咱們確定是可預知的,這個咱們須要從項目架構層面去考慮,此時在上下文上游一定還有其餘好比C#中的併發隊列或者Redis來進行攔截使其串行進行。架構

  有些人號稱是對EntityFramwork很是瞭解,認爲不就是增、刪、該、查麼,可是有的時候用出了問題就開始自我開解,我這麼用沒有任何問題啊,咱們都知道在EF 6.x中確實有不少坑,這個時候就借這個原因洗白了,這不是個人鍋,結果EF背上了無名之鍋,妄名之冤。是的,您沒有說錯,EF 6.x是有不少坑,您避開這些坑不就得了,我只能說這些人太浮於表面不瞭解基本原理就妄下結論,您到底會不會用EntityFramework啊。好了來,免說我紙上談兵,我來舉兩個具體例子,您看本身到底會不會用。併發

EntityFramework 6.x查詢

        static void Main(string[] args)
        {
            using (var ctx = new EfDbContext())
            {
                ctx.Database.Log = Console.WriteLine;

                var code = "Jeffcky";
                var order = ctx.Orders.FirstOrDefault(d => d.Code == code);           
            };
            Console.ReadKey();
        }

這樣的例子用過EF 6.x的童鞋估計用爛了吧,而後查詢出來的結果讓咱們也很是滿意至少是達到了咱們的預期,咱們來看看生成的SQL語句。app

 

請問用EF的您發現什麼沒有,在WHERE查詢條件加上了一堆沒有用的東西,我只是查詢Code等於Jeffcky的實體數據,從生成的SQL來看可查詢Code等於Jeffcky的也可查詢Code等於空的數據,要是咱們以下查詢,生成如上SQL語句我以爲纔是咱們所預期的對不對。框架

            using (var ctx = new EfDbContext())
            {
                ctx.Database.Log = Console.WriteLine;

                var code = "Jeffcky";
                var orders = ctx.Orders.Where(d => d.Code == null || d.Code == code).ToList();

            };

若是您真的會那麼一點點用EntityFramework,那麼請至少了解背後生成的SQL語句吧,這是其中之一,那要是咱們直接使用值查詢呢,您以爲是否和利用參數生成的SQL語句是同樣的呢?函數

            using (var ctx = new EfDbContext())
            {
                ctx.Database.Log = Console.WriteLine;

                var order = ctx.Orders.FirstOrDefault(d => d.Code == "Jeffcky");

            };

出乎意料吧,利用值查詢在WHERE條件上沒有過多的條件過濾,而利用參數查詢則是生成過多的條件篩選,到這裏是否是就到此爲止了呢,若是您對於參數查詢不想生成對空值的過濾,咱們在上下文構造函數中可關閉這種所謂【語義可空】判斷,以下:性能

    public class EfDbContext : DbContext
    {
        public EfDbContext() : base("name=ConnectionString")
        {
            Configuration.UseDatabaseNullSemantics = true;
        }
     }

// 摘要:
// 獲取或設置一個值,該值指示當比較兩個操做數,而它們均可能爲 null 時,是否展現數據庫 null 語義。默認值爲 false。例如:若是 UseDatabaseNullSemantics
// 爲 true,則 (operand1 == operand2) 將轉換爲 (operand1 = operand2);若是 UseDatabaseNullSemantics
// 爲 false,則將轉換爲 (((operand1 = operand2) AND (NOT (operand1 IS NULL OR operand2
// IS NULL))) OR ((operand1 IS NULL) AND (operand2 IS NULL)))。
//
// 返回結果:
// 若是啓用數據庫 null 比較行爲,則爲 true;不然爲 false。學習

在EF 6.x中對於查詢默認狀況下會進行【語義可空】篩選,經過如上分析,不知您們是否知道如上的配置呢。測試

EntityFramework 6.x更新

EF 6.x更新操做又是用熟透了吧,在EF中沒有Update方法,而在EF Core中存在Update和UpdateRange方法,您是否以爲更新又是如此之簡單呢?咱們下面首先來看一個例子,看看您是否真的會用。

        static Customer GetCustomer()
        {
            var customer = new Customer()
            {
                Id = 2,
                CreatedTime = DateTime.Now,
                ModifiedTime = DateTime.Now,
                Email = "2752154844@qq.com",
                Name = "Jeffcky1"
            };
            return customer;
        }

如上實體如咱們請求傳到後臺須要修改的實體(假設該實體在數據庫中存在哈),這裏咱們進行寫死模擬。接下來咱們來進行以下查詢,您思考一下是否能正常更新呢?

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers.FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    ctx.Customers.Attach(customer);
                    ctx.Entry(customer).State = EntityState.Modified;
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失敗");
                    }
                }

            };

首先咱們根據傳過來的實體主鍵去數據庫中查詢是否存在,若存在則將傳過來的實體附加到上下文中(由於此時請求過來的實體還未被跟蹤),而後將其狀態修改成已被修改,最後提交,解釋的是否是很是合情合理且合法,那是否是就打印更新成功了呢?

看到上述錯誤想必有部分童鞋一會兒就明白問題出在哪裏,當咱們根據傳過來的實體主鍵去數據庫查詢,此時在數據庫中存在就已被上下文所跟蹤,而後咱們又去附加已傳過來的實體且修改狀態,固然會出錯由於在上下文已存在相同的對象,此時必然會產生已存在主鍵衝突。有的童鞋想了直接將傳過來的實體狀態修改成已修改不就得了麼,以下:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                ctx.Entry(customer).State = EntityState.Modified;
                if (ctx.SaveChanges() > 0)
                {
                    Console.WriteLine("更新成功");
                }
                else
                {
                    Console.WriteLine("更新失敗");
                }
            };

如此確定能更新成功了,我想都不會這麼幹吧,要是客戶端進行傳過來的主鍵在數據庫中不存在呢(至少咱們得保證數據是已存在才修改),此時進行如上操做將拋出以下異常。

此時爲了解決這樣的問題最簡單的方法之一則是在查詢實體是否存在時直接經過AsNoTracking方法使其不能被上下文所跟蹤,這樣就不會出現主鍵衝突的問題。

 var dataBaseCustomer = ctx.Customers
                    .AsNoTracking()
                    .FirstOrDefault(d => d.Id == customer.Id);

咱們繼續往下探討 ,此時咱們將數據庫Email修改成可空(映射也要對應爲可空,不然拋出驗證不經過的異常,你懂的),以下圖:

 

而後將前臺傳過來的實體進行以下修改,不修改Email,咱們註釋掉。

        static Customer GetCustomer()
        {
            var customer = new Customer()
            {
                Id = 2,
                CreatedTime = DateTime.Now,
                ModifiedTime = DateTime.Now,
                //Email = "2752154844@qq.com",
                Name = "Jeffcky1"
            };
            return customer;
        }

咱們接着再來進行以下查詢試試看。

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .AsNoTracking()
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    ctx.Customers.Attach(customer);
                    ctx.Entry(customer).State = EntityState.Modified;
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失敗");
                    }
                }

            };

此時Email爲可空,由於咱們設置實體狀態爲Modified,此時將對實體進行全盤更新,因此對於設置實體狀態爲Modified是針對全部列更新,要是咱們只想更新指定列,那這個就很差使了,此時咱們可經過Entry().Property()...來手動更新指定列,好比以下:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .AsNoTracking()
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    ctx.Customers.Attach(customer);
                    ctx.Entry(customer).Property(p => p.Name).IsModified = true;
                    ctx.Entry(customer).Property(p => p.Email).IsModified = true;
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失敗");
                    }
                }

            };

咱們繼續往下走。除了上述利用AsNoTracking方法外使其查詢出來的實體未被上下文跟蹤而成功更新,咱們還可使用手動賦值的方式更新數據,以下:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    dataBaseCustomer.CreatedTime = customer.CreatedTime;
                    dataBaseCustomer.ModifiedTime = customer.ModifiedTime;
                    dataBaseCustomer.Email = customer.Email;
                    dataBaseCustomer.Name = customer.Name;
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失敗");
                    }
                }

            };

如上也能更新成功而不用將查詢出來的實體未跟蹤,而後將前臺傳過來的實體進行附加以及修改狀態,下面咱們刪除數據庫中建立時間和修改時間列,此時咱們保持數據庫中數據和從前臺傳過來的數據如出一轍,以下:

        static Customer GetCustomer()
        {
            var customer = new Customer()
            {
                Id = 2,
                Email = "2752154844@qq.com",
                Name = "Jeffcky1"
            };
            return customer;
        }

接下來咱們再來進行以下賦值修改,您會發現此時更新失敗的:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    dataBaseCustomer.Email = customer.Email;
                    dataBaseCustomer.Name = customer.Name;
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失敗");
                    }
                }

            };

這是爲什麼呢?由於數據庫數據和前臺傳過來的數據如出一轍,可是不會進行更新,毫無疑問EF這樣處理是明智且正確的,無需畫蛇添足更新,那咱們怎麼知道是否有不同的數據進行更新操做呢,換句話說EF怎樣知道數據未發生改變就不更新呢?咱們能夠用上下文屬性中的ChangeTacker中的HasChanges方法,若是上下文知道數據未發生改變,那麼直接返回成功,以下:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    dataBaseCustomer.Email = customer.Email;
                    dataBaseCustomer.Name = customer.Name;
                    if (!ctx.ChangeTracker.HasChanges())
                    {
                        Console.WriteLine("更新成功");
                        return;
                    }
                    if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失敗");
                    }
                }

            };

好了到此爲止咱們已經看到關於更新已經有了三種方式,彆着急還有最後一種,經過Entry().CurrentValues.SetValues()方式,這種方式也是指定更新,將當前實體的值設置數據庫中查詢出來所被跟蹤的實體的值。以下:

            using (var ctx = new EfDbContext())
            {
                var customer = GetCustomer();
                var dataBaseCustomer = ctx.Customers
                    .FirstOrDefault(d => d.Id == customer.Id);
                if (dataBaseCustomer != null)
                {
                    ctx.Entry(dataBaseCustomer).CurrentValues.SetValues(customer); if (ctx.SaveChanges() > 0)
                    {
                        Console.WriteLine("更新成功");
                    }
                    else
                    {
                        Console.WriteLine("更新失敗");
                    }
                }

            };

關於EF更新方式講了四種,其中有關細枝末節就沒有再細說可自行私下測試,不知道用過EF的您們是否四種都知道以及每一種對應的場景是怎樣的呢?對於數據更新我通常直接經過查詢進行賦值的形式,固然咱們也能夠用AutoMapper,而後經過HasChanges方法來進行判斷。

EntityFramework 6.x批量添加

對於批量添加已是EF 6.x中老掉牙的話題,可是依然有不少面試者不知道,我這裏再從新講解一次,對於那些私下不學習,不與時俱進的童鞋好歹也看看前輩們(不包括我)總經的經驗吧,不知道爲什麼這樣作,至少回答答案是對的吧。看到下面的批量添加數據代碼是否是有點想打人。

           using (var ctx = new EfDbContext())
            {
                for (var i = 0; i <= 100000; i++)
                {
                    var customer = new Customer
                    {
                        Email = "2752154844@qq.com",
                        Name = i.ToString()
                    };
                    ctx.Customers.Add(customer);
                    ctx.SaveChanges();
                }
            };

至於緣由無需我過多解釋,若是您這樣操做,那您這一天的工做大概也就是等着數據添加完畢,等啊等。再不濟您也將SaveChanges放在最外層一次性提交啊,這裏我就再也不測試,浪費時間在這上面不必,只要您稍微懂點EF原理至少會以下這麼使用。

            var customers = new List<Customer>();
            using (var ctx = new EfDbContext())
            {
                for (var i = 0; i <= 100000; i++)
                {
                    var customer = new Customer
                    {
                        Email = "2752154844@qq.com",
                        Name = i.ToString()
                    };
                    customers.Add(customer);
                }
                ctx.Customers.AddRange(customers);
                ctx.SaveChanges();
            };

若是您給個人答案如上,我仍是承認的,要是第一種真的說不過去了啊。通過如上操做依然有問題,咱們將全部記錄添加到同一上下文實例,這意味着EF會跟蹤這十萬條記錄, 對於剛開始添加的幾個記錄,會運行得很快,可是當越到後面數據快接近十萬時,EF正在追蹤更大的對象圖,您以爲恐怖不,這就是您不懂EF原理的代價,還對其進行詬病,吐槽性能能夠,至少保證您寫的代碼沒問題吧,咱們進一步優化須要關閉自調用的DetectChanges方法無需進行對每個添加的實體進行掃描。

            var customers = new List<Customer>();
            using (var ctx = new EfDbContext())
            {
                bool acd = ctx.Configuration.AutoDetectChangesEnabled;
                try
                {
                    ctx.Configuration.AutoDetectChangesEnabled = false;
                    for (var i = 0; i <= 100000; i++)
                    {
                        var customer = new Customer
                        {
                            Email = "2752154844@qq.com",
                            Name = i.ToString()
                        };
                        customers.Add(customer);
                    }
                    ctx.Customers.AddRange(customers);
                    ctx.SaveChanges();
                }
                finally
                {
                    ctx.Configuration.AutoDetectChangesEnabled = acd;
                }
            };

此時咱們經過局部關閉自調用DetectChanges方法,此時EF不會跟蹤實體,這樣將不會形成全盤掃描而使得咱們不會處於漫長的等待,如此優化將節省大量時間。若是在咱們瞭解原理的前提下知道添加數據到EF上下文中,隨着數據添加到集合中也會對已添加的數據進行全盤掃描,那咱們何不建立不一樣的上下文進行批量添加呢?未經測試在這種狀況下是否比關閉自調用DetectChanges方法效率更高,僅供參考,代碼以下:

    public static class EFContextExtensions
    {
        public static EfDbContext BatchInsert<T>(this EfDbContext context, T entity, int count, int batchSize) where T : class
        {
            context.Set<T>().Add(entity);

            if (count % batchSize == 0)
            {
                context.SaveChanges();
                context.Dispose();
                context = new EfDbContext();
            }
            return context;
        }
    }
        static void Main(string[] args)
        {
            var customers = new List<Customer>();
            EfDbContext ctx;
            using (ctx = new EfDbContext())
            {
                for (var i = 0; i <= 100000; i++)
                {
                    var customer = new Customer
                    {
                        Email = "2752154844@qq.com",
                        Name = i.ToString()
                    };
                    ctx = ctx.BatchInsert(customer, i, 100);
                }
                ctx.SaveChanges();
            };
            Console.ReadKey();
        }    

總結

不喜勿噴,敢問您到底會不會用EntityFramework啊,EF 6.x性能使人詬病可是至少得保證您寫的代碼沒問題吧,對於複雜SQL查詢能夠EF很是雞肋,可是咱們可結合Dapper使用啊,您又擔憂EF 6.x坑太多,那請用EntityFramework Core吧,您值得擁有。謹以此篇批判那些不會用EF的同行,還將EF和併發扯到一塊,EF不是用來抵抗併發,它的出現是爲了讓咱們將重心放在梳理業務對象,關注業務上,有關我對EF 6.x和EF Core 2.0理解所有集成到我寫的書《你必須掌握的EntityFramework 6.x與Core 2.0》下個月可正式購買,想了解的同行可關注下,謝謝。

後續

看了不少前輩精彩的評論,我我的以爲既然用了EF那就得提早知道這些基礎知識或者基本原理,出了問題歸結於EF,那就有點說不過去了,再者網上的前輩們在項目中總結的經驗和老外的技術文檔比比皆是,爲什麼不花點時間提早了解下是否知足項目需求呢。我在EF這方面不是專家,更談不上精通,只不過常常看看國內和國外的技術文檔,本身私下親自實踐罷了。最後總結起來一點則是選擇適合本身項目的纔是最好的,別太依賴EF,EF解決不了全部問題。

相關文章
相關標籤/搜索