SQL Server的SQL查詢不區分大小寫,而LINQ查詢區分大小寫,所以在寫LINQ代碼時需要注意的是 ——如果這段LINQ代碼將會被Entity Framework解析為SQL語句(LINQ to Entities),則不 用考慮大小寫問題;如果這段LINQ代碼在內存中執行,就要考慮大小寫的問題。
比如下面的LINQ to Entities(不用考慮大小寫):
//代碼自來CNBlogsTagService _unitOfWork.Set<Tag>().Where(x => tagNames.Contains(x.TagName))
而如果是LINQ,則需要這麼寫(通過StringComparer.OrdinalIgnoreCase忽略大小寫):
content.Tags.RemoveAll(x => tagNames.Contains(x.TagName, StringComparer.OrdinalIgnoreCase) == false);
這種不一致帶來的問題是——同樣是寫LINQ,你卻要區別對待,你要考慮這段LINQ代碼是 在內存中執行,還是會被解析為SQL執行。
這個大小寫問題是大家熟知的,解決起來也不困難。
而我們最近在實際項目中遇到了一個神奇的問題,與大小寫問題是同一類問題——在SQL Server中進行SQL查詢時竟然不區分全角半角,而在LINQ中是區分的。
下面我們通過CNBlogsTagService項目(一個基於Entity Framework實現的為前端應用提供Tag服務的 後端服務)中的一個實際場景感受一下。
先看一段LINQ to Entities代碼:
public List<Tag> GetTags(IEnumerable<string> tagNames) { var existedTags = _unitOfWork.Set<Tag>().Where(x => tagNames.Contains (x.TagName)).ToList(); //... }
上面的代碼是根據TagName從數據庫中查詢記錄,然後得到對應的Tag實體。
我們遭遇問題時,tagNames的值是{ "C++" },注意這裡的加號是全角,數據庫中存儲 的TagName的值是"c++"(這裡的加號是半角)。上面的代碼執行後得到的結果是 ——existedTags[0].TagName的值為"c++"。SQL查詢竟然能自動匹配全角半角, 當時發現這個也是第1次知道這回事,不由感歎——好智能的SQL Server。
但是這種智能帶來的不一致卻讓我們經歷了一次艱難的問題排查過程。
再看後續的LINQ代碼:
var createdTags = tagNames.Where(x => existedTags.Select(y => y.TagName) .Contains(x, StringComparer.InvariantCultureIgnoreCase) == false) .Select(x => new Tag { TagName = x }).ToList();
這段代碼是在內存中進行LINQ查詢操作的代碼,用途是找出tagNames(類型是 IEnumerable<string>)中存在,而且existedTags(EF的實體)不存在的TagName(也就是找出在 數據庫中不存在的TagName)。
根據之前的場景,tagNames的值是{ "C++" },existedTags[0].TagName的值是 "c++"。既然數據庫中已存在這個Tag,我們所期望的是createdTags中沒有數據,但是由於 LINQ區分全角半角,得到的結果卻是——createdTags[0].TagName的值為"C++" ,在通過Entity Framework進行SaveChanges時引發了異常:
System.Data.SqlClient.SqlException: Cannot insert duplicate key row in object 'dbo.Tags' with unique index 'IX_Tags_TagName'. The duplicate key value is (C++).
本來這裡的代碼的目的是如果指定名稱的Tag在數據庫中不存在,就創建它,並保存至數據庫。對應 現在的場景,變成了——"C++"這個Tag在數據庫中存在嗎?數據庫說:存在, 名叫"c++";{ "C++" } 中有哪些是 { "c++" }所沒有的?LINQ說: "C++";於是,EF將"C++"保存數據庫,數據庫卻說:我這已經有了c++, "C++"請滾開。於是就有了上面的異常。
問題就出在SQL與LINQ的不一致行為上。如果事先不知道不一致的情況,出現bug時,往往最難對付! 在博客中寫出來看上去問題似乎很簡單,但我們糾纏於這個問題時,猜測了成千上萬的原因,也沒想到 是這個原因。最後發現時不由感歎——真是一次奇遇!
那如何解決這個問題呢?
我們想到的最簡單的方法是在LINQ查詢時忽略全角半角。
那如何以最簡單的方法實現在LINQ查詢時忽略全角半角呢?
園子裡2005年空軍寫的一篇博文(C#中直接調用VB.NET的函數,兼論半角與全角、簡繁體中文互相轉 化)讓我們很快有了答案——在C#中調用VB.NET中的函數Strings.StrConv(x, VbStrConv.Narrow);
具體實現方法如下:
1. 在Visual Studio中為項目添加Microsoft.VisualBasic的引用
2. 將上面的LINQ代碼改為如下的代碼:
var createdTags = tagNames.Where(x => existedTags.Select(y => Strings.StrConv (y.TagName, VbStrConv.Narrow)) .Contains(Strings.StrConv(x, VbStrConv.Narrow), StringComparer.InvariantCultureIgnoreCase) == false) .Select(x => new Tag { TagName = x }).ToList();
寫好這篇博客後,突然覺得也算不上什麼奇遇記,可能很多朋友早就知道了這個情況。標題黨只是為 了表達一下解決問題後的那種興奮的感覺。
解決問題是一種快樂,那有沒有比解決問題更快樂的事情呢?有,那就是在解決問題後寫一篇博客!
查看本欄目