文章出處

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();

寫好這篇博客后,突然覺得也算不上什么奇遇記,可能很多朋友早就知道了這個情況。標題黨只是為了表達一下解決問題后的那種興奮的感覺。

解決問題是一種快樂,那有沒有比解決問題更快樂的事情呢?有,那就是在解決問題后寫一篇博客!


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()