1. 程式人生 > 實用技巧 >EF底層操作注意點、批量操作、更新

EF底層操作注意點、批量操作、更新

轉載https://www.cnblogs.com/CreateMyself/p/8989983.html

前言

一直以來寫的博文都是比較溫婉型的博文,今天這篇博文算是一篇批判性博文,有問題歡迎探討,如標題,你到底會不會用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語句。

請問用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 = "[email protected]",
                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 = "[email protected]",
                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 = "[email protected]",
                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 = "[email protected]",
                        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 = "[email protected]",
                        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 = "[email protected]",
                            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 = "[email protected]",
                        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解決不了所有問題。