前言:前面总结了一些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. 使用原生JS实现一个风箱式的demo,并封装了一个运动框架

    声明,该DEMO依托于某个培训机构中,非常感谢这个培训结构.话不多说,现在开始改demo的制作. 首先,在前端的学习过程中,轮播图是我们一定要学习的,所以为了更加高效的实现各种轮播图,封装了一个运动的 ...

  2. SharePoint 2013 状态机工作流之UpdateItemActivity

    没什么可说的,一个Activity的使用介绍,其他类似的Activity也可以参考这个使用. 1.添加ApplyActivation和UpdateItemActivity,在onWorkflowAct ...

  3. SharePoint 2013 状态机工作流之日常报销示例

    简单介绍下状态机工作流,状态机工作流提供了一系列的状态.工作流从初始状态开始,到终止状态结束.两个状态之间定义行为进行过渡.通常情况下,状态机工作流对事件作出反应,事件的发生将会使状态发生改变. 1. ...

  4. O365(世纪互联)SharePoint 之站点个性化

    前言 上一篇文章中,我们简单介绍了如何使用O365中SharePoint Online文档库,SharePoint Online的优点就是提供给我们很多非常方便开箱即用的功能,让我们快速的搭建站点,方 ...

  5. 网站部署 HTTPS 中需要做的事情

    这篇文章首发于我的个人网站:听说 - https://tasaid.com/,建议在我的个人网站阅读,拥有更好的阅读体验. 这篇文章与 博客园 和 Segmentfault 共享. 前端开发QQ群:3 ...

  6. Emacs学习心得之 LaTeX编辑

    目录 1. 前言 2. texlive的安装 3. AUCTEX的安装和配置 4. RefTEX的安装和配置 一.前言 本篇博文记录了Emacs下LaTeX编辑环境的搭建,参考一下文章: http:/ ...

  7. [Android]Android端ORM框架——RapidORM(v1.0)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4748077.html  Android上主流的ORM框架有很多 ...

  8. Linux0.11内核--内存管理之2.配合fork

    [版权所有,转载请注明出处.出处:http://www.cnblogs.com/joey-hua/p/5598451.html ] 在上一篇的fork函数中,首先一上来就调用get_free_page ...

  9. org.hibernate.HibernateException: No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here

    org.hibernate.HibernateException: No Hibernate Session bound to thread, and configuration does not a ...

  10. 职业规划:管理vs技术

    "每个人都身怀天赋,但如果用会不会爬树能力来评判一只鱼,那它这辈子都会觉得自己是条蠢鱼" - 阿尔伯特.爱因斯坦 我想我为这篇博客已经准备了很长时间.但是看起了我还一直挣扎我该往哪 ...