前言:前面总结了一些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. node学习笔记

    一.准备(github地址) 什么是Javascript? ... Javascript能做什么? ..... 浏览器中的Javascript可以做什么? 操作DOM(增删改查) AJAX/跨域 BO ...

  2. MSSQL 分页

    使用数据库分页返回用户数据有如下好处:1.减少服务器磁盘系统地读取压力2.减少网络流量,减轻网络压力3.减轻客户端显示数据的压力4.提高处理效率. 一般而言分页处理分为两种:应用程序中的分页(查询出所 ...

  3. 网页端实现input数字输入框

    实现input输入框只能输入数字的效果: <input type="text" name="" id="phoneNum" value ...

  4. 安装Portal for ArcGIS时如何正确配置HTTPS证书

    SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持.SSL协议可分为两层: SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为 ...

  5. 使用 UICollectionView 实现日历签到功能

    概述 在 App 中,日历通常与签到功能结合使用.是提高用户活跃度的一种方式,同时,签到数据中蕴含了丰富的极其有价值的信息.下面我们就来看看如何在 App 中实现日历签到功能. 效果图 ..... 思 ...

  6. Mysql数据库的基本概念和架构

    数据库 1.键:主键是表中的标志列.一个键可能由几列组成.可以使用键作为表格之间的引用. CustomerID是Customers表的主键,当它出现在其他表,例如Orders表中的时候就称它为外键. ...

  7. [Oracle]快速插入大量(100w)数据

    背景:无论在开发调试或者软件测试中,测试数据的准备是调试/测试执行前重要和必要的一个环节,因此以下几种方式可以快速插入大量数据: 第一种方法: declare   -- Local variables ...

  8. SqlServer--查询案例

    use  MyDataBase1 -- * 表示显示所有列 -- 查询语句没有加where条件表示查询所有行 select *from TblStudent ---只查询表中的部分列 select t ...

  9. String.Empty、null、“” 区别

    概念准备: 1.引用类型是将对象是实际数据保存在堆中, 将对象在堆中的地址保存在栈中. 2.值类型直接将实际数据存放在堆中,不会将对象在堆中的地址保存在栈中. 一.String.Empty和" ...

  10. Oracle system identifier(SID) "xxx" alread exits. Specify another SID

    案例环境: 操作系统    :Oracle Linux Server release 5.7 64 bit 数据库版本:Oracle Database 10g Release 10.2.0.4.0 - ...