前言:前面总结了一些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的理解,请帮忙推荐,博主将会继续努力。

疑难杂症——EF+Automapper引发的查询效率问题解析的更多相关文章

  1. EF 数据查询效率对比

    优化的地方: 原地址:https://www.cnblogs.com/yaopengfei/p/9226328.html ①:如果仅是查询数据,并不对数据进行增.删.改操作,查询数据的时候可以取消状态 ...

  2. 深入理解 EF Core:使用查询过滤器实现数据软删除

    原文:https://bit.ly/2Cy3J5f 作者:Jon P Smith 翻译:王亮 声明:我翻译技术文章不是逐句翻译的,而是根据我自己的理解来表述的.其中可能会去除一些本人实在不知道如何组织 ...

  3. SQL 提高查询效率

    1.关于SQL查询效率,100w数据,查询只要1秒,与您分享: 机器情况p4: 2.4内存: 1 Gos: windows 2003数据库: ms sql server 2000目的: 查询性能测试, ...

  4. mysql 实战 or、in与union all 的查询效率

    OR.in和union all 查询效率到底哪个快. 网上很多的声音都是说union all 快于 or.in,因为or.in会导致全表扫描,他们给出了很多的实例. 但真的union all真的快于o ...

  5. 提高SQL查询效率(SQL优化)

    要提高SQL查询效率where语句条件的先后次序应如何写 http://blog.csdn.net/sforiz/article/details/5345359   我们要做到不但会写SQL,还要做到 ...

  6. 提高SQL的查询效率

    1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引.   2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使 ...

  7. SQL查询效率:100w数据查询只需要1秒钟

    G os: windows 数据库: ms sql server 目的: 查询性能测试,比较两种查询的性能 SQL查询效率 step by step -- setp . -- 建表 create ta ...

  8. sql 查询效率

    1. SQL优化的原则是:将一次操作需要读取的BLOCK数减到最低,即在最短的时间达到最大的数据吞吐量.调整不良SQL通常可以从以下几点切入: 检查不良的SQL,考虑其写法是否还有可优化内容 检查子查 ...

  9. mysql in 子查询 效率慢 优化(转)

    mysql in 子查询 效率慢 优化(转) 现在的CMS系统.博客系统.BBS等都喜欢使用标签tag作交叉链接,因此我也尝鲜用了下.但用了后发现我想查询某个tag的文章列表时速度很慢,达到5秒之久! ...

随机推荐

  1. 前端Demo常用库文件链接

    <!doctype html><html><head> <meta charset="UTF-8"> <title>前端 ...

  2. iOS 线程间的通信 (GCD)

    1.从网络上 下载一张图片将它显示在view上 - (void)imageDownload { dispatch_async(dispatch_get_global_queue(DISPATCH_QU ...

  3. 配置nginx支持ssl服务器—HTTPS

    下文摘自: http://docs.bigbluebutton.org/install/install.html     Configuring HTTPS on BigBlueButtonAncho ...

  4. How To Collect ULS Log from SharePoint Farm

    We can use below command to collect SharePoint ULS log from all servers in the Farm in PowerShell. M ...

  5. iOS开发之开源项目链接

    1. Coding iOS 客户端 Coding官方客户端. 笔者强烈推荐的值得学习的完整APP.GitHub - Coding/Coding-iOS: Coding iOS 客户端源代码 2. OS ...

  6. 基于Ruby的watir-webdriver自动化测试方案与实施(二)

    接着基于Ruby的watir-webdriver自动化测试方案与实施(一) http://www.cnblogs.com/Javame/p/4159360.html 继续 ... ...   回顾 软 ...

  7. ajax参数设置略解

    通过ajax可以直接由页面访问到服务器.做到不刷新页面,就能刷新数据,为开发带来很大的便利. 1.ajax方式的参数及其功能: $.ajax({ type : "POST", // ...

  8. SqlHelper类

    using System; using System.Collections; using System.Collections.Generic; using System.Data; using S ...

  9. JDK1.3安装出现/lib/ld-linux.so.2: bad ELF interpreter: No such file or directory Done.

    今天是出道以来第一次安装JDK1.3,大学的时候接触的也已是JDK1.4,而且是在Red Hat Enterprise Linux Server release 6.6上,安装JDK1.3是由于软件组 ...

  10. Linux安装DBI/DBD-ORACLE

    本文只是学习如何配置PERL DBI.PERL DBD时,整理的一个学习实践文档,大部分参考网上资料,详情请见下面参考资料. PERL对数据库的支持广而且全,几乎所有的主流数据库都有与之相应的PERL ...