前言:前面總結了一些WebApi裡面常見問題的解決方案,本來打算來分享下oData+WebApi的使用方式的,奈何被工作所困,隻能将此往後推了。今天先來看看EF和AutoMapper聯合使用的一個問題。
最近兩周一直在解決一個問題:使用Automapper将EF的Model轉換成DTO的Model,資料量隻有幾百條,但是導航屬性比較多,執行 Mapper.Map<List<TM_STATION>, List<DTO_TM_STATION>>(lstEFModel) 這一句的時候需要耗時十多秒鐘左右,簡直到了用不了的節奏。于是乎各種找資料,好不容易解決了,今天來簡單記錄下這個過程,也總結下EF裡面的一些細節性的東西。
項目使用EF 5.0,為了避免UI裡面直接調用EF的Model,我們定義了一個中間的實體層DTO,每次查詢資料的時候首先通過查詢得到EF的Model,然後通過Automapper将EF的Model扁平化轉換成DTO的model,就是這麼一個簡單的過程。為了模拟項目實際場景,部落客随便寫了一個控制台程式,主要的代碼流程如下:
三次測試的結果如下:

結果顯示:82條資料,總共需要6秒左右。上述代碼可以看到,這個實體導航屬性很多,并且其中還有某些導航屬性存在二級導航屬性,盡管如此,82條資料需要6s轉換,這個肯定是需要優化的。
為什麼會這麼慢呢?剛開始,部落客打算從Automapper下手,在想是不是Automapper元件的問題,可是查了一圈資料後發現,最新版的Automapper都是這樣用的啊,就連官方文檔也是這樣寫的,并且園子裡其他人也有這樣用,也沒聽說性能損耗這麼嚴重的。排除了Automapper的原因,剩下的就是EF了。
1、剛開始,猜想會不會在查詢導航屬性的時候實時去資料庫取的呢?要不然不可能82條資料要這麼久。于是乎做了下面的嘗試:
結果抛了異常:
我們用using就EF的上下文對象包起來,表示出了using之後,上下文對象就自動釋放,可是在Automapper轉換的時候報了“對象已經釋放”的異常,這正好說明我們之前的猜想是正确的!由于EF預設延時加載(context.Configuration.LazyLoadingEnabled)是開啟的,每次去取資料的時候,導航屬性都不會被直接取出來。也就是說,Automapper轉換的時候是需要資料庫連接配接的,每個對象轉換的時候導航屬性需要通過這個連接配接實時去資料庫取。難怪這麼慢呢,82條記錄,從資料庫取的次數那得有多少次,吓死寶寶了。知道了這個原因,就曉得努力的方向了。
2、知道了上面的原因,部落客把關注點放在了AsNoTracking()的上面。将其轉到定義看了下:
大概的意思是,加了AsNoTracking()之後,每次的結果不會往DBContext中緩存,換言之,每次都是實時去資料庫取最新的,原來罪魁禍首在這裡。那當初為什麼查詢的時候要加上AsNoTracking()這個東西呢,部落客網上查了下,它的作用主要有兩個:
提高查詢效率。不會緩存就意味着每次去資料庫裡面取,這樣肯定能夠提高查詢效率;
保證了資料的實時性。也就是說,每次去資料庫裡面取到的結果都是最新的,這樣能夠保證資料的實時性。這個一般用在同一個上下文的情況,如果CURD每次都是一個不同的上下文,就沒有這個必要了。
通過上面的嘗試,貌似找到了問題的緣由,是不是這樣呢?我們來試一試,其他代碼都不變,僅僅把AsNoTracking()去掉。
還是來看看三次的測試結果
性能得到不少提升。
可是考慮到資料量并不大,感覺1.5秒左右還是不能令人滿意,還想再次優化下。通過上文可以,我們反向了解,去掉了AsNoTracking()之後,每次查詢都會在System.Data.Entity.DbContext對象中緩存。有了這個理論做基礎,部落客去掉AsNoTracking()之後,再次按照上面的代碼使用了using測試,結果還是和上文相同:抛了對象已釋放的異常。這說明轉換導航屬性是從DBContext緩存中取得,如果DBContext對象已經釋放,自然取不到對應的導航屬性。
到這一步,部落客是這樣了解EF機制的:為了保證查詢的效率,EF會自動啟用延時加載,所有的導航屬性都需要在調用的時候去資料庫或者上下文對象的緩存裡面去取。那麼,是否有一次取出所有導航屬性的機制呢?考慮到這種情況,微軟為我們提供了Include方法,我們需要哪些導航屬性,可以使用Include将其查出,我們來看看最後改造的代碼:
三次測試結果:
代碼釋疑:優化做到這一步基本就可以了。有園友可能又有了新的疑惑,确實,這樣做Automapper的轉換是可以了,因為需要的導航屬性已經查詢到了記憶體裡面,在記憶體裡面做這些轉換是很快的,但是,你考慮過EF查詢的性能了嗎?如果你将所有的導航屬性都查出來,那麼當查詢的資料量大了之後豈不是會很慢!這就是接下來想要說明的幾點:
優化需要做到哪一步根據實際情況,如果你的項目對性能要求不太高,上面的1.5秒可以接受,那麼我們直接用上面的那種方案即可。
如果确實對查詢和轉換性能要求都很高,并且你的系統資料量又比較大,那麼建議從兩個方面同時下手,查詢方面使用延時加載;對象轉換方面,你可以使用EmitMapper代替Automapper,為了效率更高,甚至你可以手工映射,關于映射工具的效率,可以看看此篇。
EF預設是延時加載(懶加載)的,使用Include是一種實時加載的方式,如果你不需要使用導航屬性裡面的東西,建議使用懶加載。
以上通過一次查詢優化簡單分析了下EF的一些運作機制,文中所有觀點來自部落客自己的了解,如果有誤,歡迎園友們指出,多謝。如果這篇文章能幫助你加深對EF的了解,請幫忙推薦,部落客将會繼續努力。