疑難雜症——EF+Automapper引發的查詢效率問題解析


前言:前面總結了一些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,就是這么一個簡單的過程。為了模擬項目實際場景,博主隨便寫了一個控制台程序,主要的代碼流程如下:

        static void Main(string[] args)
        {
       //1.創建Automapper的映射,並指定導航屬性的轉換對應關系 var config = new MapperConfiguration(cfg => { cfg.CreateMap<TM_STATION, DTO_TM_STATION>() .ForMember(dto => dto.TM_PLANT_ID, (map) => map.MapFrom(m => m.TM_WORKSHOP.TM_PLANT.TM_PLANT_ID)) .ForMember(dto => dto.NAME_C, (map) => map.MapFrom(m => m.TM_WORKSHOP.NAME_C)) .ForMember(dto => dto.NAME_C1, (map) => map.MapFrom(m => m.TM_WORKSHOP.TM_PLANT.NAME_C)) .ForMember(dto => dto.UlocName, (map) => map.MapFrom(m => m.TM_ULOC.NAME)) .ForMember(dto => dto.ArtName, (map) => map.MapFrom(m => m.TM_ART_LINE.NAME_C)) .ForMember(dto => dto.LineName, (map) => map.MapFrom(m => m.TM_LINE.NAME_C)) .ForMember(dto => dto.WorkShop, (map) => map.MapFrom(m => m.TM_WORKSHOP.NAME_C)); }); var Mappers = config.CreateMapper();

       //2.創建EF的上下文對象並查詢得到EF的Model集合(這里是測試的Demo,所以直接new的EF的DBContext,實際項目中多了一個倉儲) var context = new Entities(); var lstEFModel = context.Set<TM_STATION>().AsNoTracking().ToList(); //3.開啟計時,使用AutoMapper轉換對象 System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); var listd = Mappers.Map<List<DTO_TM_STATION>>(lstEFModel); sw.Stop(); var s = sw.ElapsedMilliseconds; Console.WriteLine("總共轉換" + lstEFModel.Count + "條數據。轉換耗時:" + s + "毫秒"); Console.ReadKey(); }

三次測試的結果如下:

結果顯示:82條數據,總共需要6秒左右。上述代碼可以看到,這個實體導航屬性很多,並且其中還有某些導航屬性存在二級導航屬性,盡管如此,82條數據需要6s轉換,這個肯定是需要優化的。

二、原因分析

為什么會這么慢呢?剛開始,博主打算從Automapper下手,在想是不是Automapper組件的問題,可是查了一圈資料后發現,最新版的Automapper都是這樣用的啊,就連官方文檔也是這樣寫的,並且園子里其他人也有這樣用,也沒聽說性能損耗這么嚴重的。排除了Automapper的原因,剩下的就是EF了。

1、剛開始,猜想會不會在查詢導航屬性的時候實時去數據庫取的呢?要不然不可能82條數據要這么久。於是乎做了下面的嘗試:

        static void Main(string[] args)
        {            
            //1.創建Automapper的映射,並指定導航屬性的轉換對應關系
            var config = new MapperConfiguration(cfg =>
            {
                cfg.CreateMap<TM_STATION, DTO_TM_STATION>()
                 .ForMember(dto => dto.TM_PLANT_ID, (map) => map.MapFrom(m => m.TM_WORKSHOP.TM_PLANT.TM_PLANT_ID))
                .ForMember(dto => dto.NAME_C, (map) => map.MapFrom(m => m.TM_WORKSHOP.NAME_C))
                .ForMember(dto => dto.NAME_C1, (map) => map.MapFrom(m => m.TM_WORKSHOP.TM_PLANT.NAME_C))
                .ForMember(dto => dto.UlocName, (map) => map.MapFrom(m => m.TM_ULOC.NAME))
                .ForMember(dto => dto.ArtName, (map) => map.MapFrom(m => m.TM_ART_LINE.NAME_C))
                .ForMember(dto => dto.LineName, (map) => map.MapFrom(m => m.TM_LINE.NAME_C))
                .ForMember(dto => dto.WorkShop, (map) => map.MapFrom(m => m.TM_WORKSHOP.NAME_C));
            });
            var Mappers = config.CreateMapper();
            
            //2.創建EF的上下文對象並查詢得到EF的Model集合(這里是測試的Demo,所以直接new的EF的DBContext,實際項目中多了一個倉儲)
            var lstEFModel = new List<TM_STATION>();
            using (var context = new Entities())
            {
                lstEFModel = context.Set<TM_STATION>().AsNoTracking().ToList();
            }

            //3.開啟計時,使用AutoMapper轉換對象
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            var listd = Mappers.Map<List<DTO_TM_STATION>>(lstEFModel);
            sw.Stop();
            var s = sw.ElapsedMilliseconds;
            Console.WriteLine("總共轉換" + lstEFModel.Count + "條數據。轉換耗時:" + s + "毫秒");

            Console.ReadKey();
        }

結果拋了異常:

我們用using就EF的上下文對象包起來,表示出了using之后,上下文對象就自動釋放,可是在Automapper轉換的時候報了“對象已經釋放”的異常,這正好說明我們之前的猜想是正確的!由於EF默認延時加載(context.Configuration.LazyLoadingEnabled)是開啟的,每次去取數據的時候,導航屬性都不會被直接取出來。也就是說,Automapper轉換的時候是需要數據庫連接的,每個對象轉換的時候導航屬性需要通過這個連接實時去數據庫取。難怪這么慢呢,82條記錄,從數據庫取的次數那得有多少次,嚇死寶寶了。知道了這個原因,就曉得努力的方向了。

2、知道了上面的原因,博主把關注點放在了AsNoTracking()的上面。將其轉到定義看了下:

        // 摘要: 
        //     返回一個新查詢,其中返回的實體將不會在 System.Data.Entity.DbContext 中進行緩存。
        //
        // 返回結果: 
        //     應用了 NoTracking 的新查詢。
        public DbQuery<TResult> AsNoTracking();

大概的意思是,加了AsNoTracking()之后,每次的結果不會往DBContext中緩存,換言之,每次都是實時去數據庫取最新的,原來罪魁禍首在這里。那當初為什么查詢的時候要加上AsNoTracking()這個東西呢,博主網上查了下,它的作用主要有兩個:

  1. 提高查詢效率。不會緩存就意味着每次去數據庫里面取,這樣肯定能夠提高查詢效率;
  2. 保證了數據的實時性。也就是說,每次去數據庫里面取到的結果都是最新的,這樣能夠保證數據的實時性。這個一般用在同一個上下文的情況,如果CURD每次都是一個不同的上下文,就沒有這個必要了。

三、解決方案嘗試

通過上面的嘗試,貌似找到了問題的緣由,是不是這樣呢?我們來試一試,其他代碼都不變,僅僅把AsNoTracking()去掉。

        static void Main(string[] args)
        {            
            //1.創建Automapper的映射,並指定導航屬性的轉換對應關系
            var config = new MapperConfiguration(cfg =>
            {
                cfg.CreateMap<TM_STATION, DTO_TM_STATION>()
                 .ForMember(dto => dto.TM_PLANT_ID, (map) => map.MapFrom(m => m.TM_WORKSHOP.TM_PLANT.TM_PLANT_ID))
                .ForMember(dto => dto.NAME_C, (map) => map.MapFrom(m => m.TM_WORKSHOP.NAME_C))
                .ForMember(dto => dto.NAME_C1, (map) => map.MapFrom(m => m.TM_WORKSHOP.TM_PLANT.NAME_C))
                .ForMember(dto => dto.UlocName, (map) => map.MapFrom(m => m.TM_ULOC.NAME))
                .ForMember(dto => dto.ArtName, (map) => map.MapFrom(m => m.TM_ART_LINE.NAME_C))
                .ForMember(dto => dto.LineName, (map) => map.MapFrom(m => m.TM_LINE.NAME_C))
                .ForMember(dto => dto.WorkShop, (map) => map.MapFrom(m => m.TM_WORKSHOP.NAME_C));
            });
            var Mappers = config.CreateMapper();
            
            //2.創建EF的上下文對象並查詢得到EF的Model集合(這里是測試的Demo,所以直接new的EF的DBContext,實際項目中多了一個倉儲)
            var context = new Entities();
            var lstEFModel = context.Set<TM_STATION>().ToList();

            //3.開啟計時,使用AutoMapper轉換對象
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            var listd = Mappers.Map<List<DTO_TM_STATION>>(lstEFModel);
            sw.Stop();
            var s = sw.ElapsedMilliseconds;
            Console.WriteLine("總共轉換" + lstEFModel.Count + "條數據。轉換耗時:" + s + "毫秒");

            Console.ReadKey();
        }

 還是來看看三次的測試結果

性能得到不少提升。

可是考慮到數據量並不大,感覺1.5秒左右還是不能令人滿意,還想再次優化下。通過上文可以,我們反向理解,去掉了AsNoTracking()之后,每次查詢都會在System.Data.Entity.DbContext對象中緩存。有了這個理論做基礎,博主去掉AsNoTracking()之后,再次按照上面的代碼使用了using測試,結果還是和上文相同:拋了對象已釋放的異常。這說明轉換導航屬性是從DBContext緩存中取得,如果DBContext對象已經釋放,自然取不到對應的導航屬性。

到這一步,博主是這樣理解EF機制的:為了保證查詢的效率,EF會自動啟用延時加載,所有的導航屬性都需要在調用的時候去數據庫或者上下文對象的緩存里面去取。那么,是否有一次取出所有導航屬性的機制呢?考慮到這種情況,微軟為我們提供了Include方法,我們需要哪些導航屬性,可以使用Include將其查出,我們來看看最后改造的代碼:

        static void Main(string[] args)
        {            
            //1.創建Automapper的映射,並指定導航屬性的轉換對應關系
            var config = new MapperConfiguration(cfg =>
            {
                cfg.CreateMap<TM_STATION, DTO_TM_STATION>()
                 .ForMember(dto => dto.TM_PLANT_ID, (map) => map.MapFrom(m => m.TM_WORKSHOP.TM_PLANT.TM_PLANT_ID))
                .ForMember(dto => dto.NAME_C, (map) => map.MapFrom(m => m.TM_WORKSHOP.NAME_C))
                .ForMember(dto => dto.NAME_C1, (map) => map.MapFrom(m => m.TM_WORKSHOP.TM_PLANT.NAME_C))
                .ForMember(dto => dto.UlocName, (map) => map.MapFrom(m => m.TM_ULOC.NAME))
                .ForMember(dto => dto.ArtName, (map) => map.MapFrom(m => m.TM_ART_LINE.NAME_C))
                .ForMember(dto => dto.LineName, (map) => map.MapFrom(m => m.TM_LINE.NAME_C))
                .ForMember(dto => dto.WorkShop, (map) => map.MapFrom(m => m.TM_WORKSHOP.NAME_C));
            });
            var Mappers = config.CreateMapper();
            
            //2.創建EF的上下文對象並查詢得到EF的Model集合(這里是測試的Demo,所以直接new的EF的DBContext,實際項目中多了一個倉儲)
            var context = new Entities();
            var lstEFModel = context.Set<TM_STATION>()
                .Include("TM_WORKSHOP")
                .Include("TM_LINE")
                .Include("TM_ART_LINE")
                .Include("TM_ULOC")
                .ToList();
           //3.開啟計時,使用AutoMapper轉換對象
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            var listd = Mappers.Map<List<DTO_TM_STATION>>(lstEFModel);
            sw.Stop();
            var s = sw.ElapsedMilliseconds;
            Console.WriteLine("總共轉換" + lstEFModel.Count + "條數據。轉換耗時:" + s + "毫秒");

            Console.ReadKey();
        }

三次測試結果:

代碼釋疑:優化做到這一步基本就可以了。有園友可能又有了新的疑惑,確實,這樣做Automapper的轉換是可以了,因為需要的導航屬性已經查詢到了內存里面,在內存里面做這些轉換是很快的,但是,你考慮過EF查詢的性能了嗎?如果你將所有的導航屬性都查出來,那么當查詢的數據量大了之后豈不是會很慢!這就是接下來想要說明的幾點:

  1. 優化需要做到哪一步根據實際情況,如果你的項目對性能要求不太高,上面的1.5秒可以接受,那么我們直接用上面的那種方案即可。
  2. 如果確實對查詢和轉換性能要求都很高,並且你的系統數據量又比較大,那么建議從兩個方面同時下手,查詢方面使用延時加載;對象轉換方面,你可以使用EmitMapper代替Automapper,為了效率更高,甚至你可以手工映射,關於映射工具的效率,可以看看此篇
  3. EF默認是延時加載(懶加載)的,使用Include是一種實時加載的方式,如果你不需要使用導航屬性里面的東西,建議使用懶加載。

四、總結

以上通過一次查詢優化簡單分析了下EF的一些運行機制,文中所有觀點來自博主自己的理解,如果有誤,歡迎園友們指出,多謝。如果這篇文章能幫助你加深對EF的理解,請幫忙推薦,博主將會繼續努力。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM