EntityFramework 优化建议

做者:@nele
本文为做者原创,转载请注明出处:http://www.javashuo.com/article/p-vpitxgnf-ev.htmlhtml


目录

1.EF实现指定字段的更新
2.批量查询功能
3.查询缓存功能

Entity Framework目前最新版本是6.1.3,固然Entity Framework 7 目前仍是预览版,并不能投入正式生产环境,估计正式版16年第一季度会出来,了解过EF7的部分新特性后,仍是狠狠期待一下滴。git

EF性能问题一直为开发者所诟病,最让人纠结的也是这块,因此这次我也来谈谈EF的性能优及建议。既然是把优化点列举出来,可能有些地方关于底层的知识就不会介绍的太深入,权当抛砖引玉吧。github

先说说EF性能优化工具MiniProfiler,(不过也能够直接用Sqlserver profiler)MiniProfiler是StackOverFlow团队设计的一款对.net的性能分析小程序。sql

在这里咱们可使用MiniProfiler嵌入页面查看页面处理的周期和Sql语句执行的周期及Sql语句。能够经过Nuget下载MiniProfiler和MiniProfiler.EF而后进行安装与配置(具体操做暂不细说)。docker

由于做为宇宙级的开发工具VS2015已经提供了一个更为直接明了的方式,那就是“诊断工具”,具体打开的位置数据库

enter description here

此工具能更为直观的将EF操做数据库的SQL语句所列举出来。如我要查询角色表数据小程序

EntityDB db = new EntityDB(); db.Role.Where(a => a.Id > 1).Select(a => a.Id).ToList(); 

查看工具显示c#

enter description here

查看“执行Reader”能够看到SQL语句缓存

enter description here

方便你根据查询语句修改你的查询表达式及显示model.安全

如下为这次目录列表:

下面开始一一介绍

1.使用最新版的EF

使用最新版的EF正式版本代替老的版本(除旧迎新哈哈),毕竟EF是微软所重视的主流数据操做库,每次升级版本优化效果都挺明显的。

2. 禁用延迟加载

若使用延迟加载遍历单个Model下的某一集合属性,以下面的例子:

var user = db.Person.Single(a => a.Id == 1); foreach (var role in user.Roles) { Console.WriteLine(role.Name); } 

每次咱们须要访问属性Role.Name的时候都会访问数据,这样累加起来的开销是很大的。

EF默认使用延迟加载获取导航属性关联的数据。

做为默认配置的延迟加载,须要知足如下几个条件:

  1. context.Configuration.ProxyCreationEnabled = true;

  2. context.Configuration.LazyLoadingEnabled = true;

  3. 导航属性被标记为virtual

这三个条见缺一不可。所以能够选择性禁用全局延迟加载或者是某一属性的延迟加载.

3.使用贪婪加载(又叫预加载就是数据库的多表查询)

这点其实也跟上面的同样响应了一个原则:尽可能的减小数据库的访问次数,

var user = db.Person.Include(a=>a.Roles); 

一次查询将UserProfile与其Role表数据查询出来

4.了解 IQueryable,IEnumerable的区别

IQueryable返回的是查询表达式,也就是说生成了SQL查询语句可是却尚未与数据库进行交互。

IEnumerable则是已经执行查询数据库的操做且数据保存在了内存中

因此在进行条件拼接的时候必定要在IQueryable类型后面追加Where条件语句,而不是等到ToList以后再开始写条件

错误的写法:

db.Person.ToList().Where(a => a.IsDeleted == false); 

正确的写法:

db.Person.Where(a => a.IsDeleted == false).ToList(); 

这些写法的意思就是把数据条件拼凑好,再访问数据库。不然从数据库获取所有数据后再过滤,假如数据很庞大几十万,那后果可想而知!

5.优化操做AsNoTracking()与Attach

对于只读操做,强烈建议使用AsNoTracking进行数据获取,这样省去了访问EF Context的时间,会大大下降数据获取所需的时间。

同时因为没有受到上下文的跟踪缓存,所以取得的数据也是及时最新的,更利于某些对数据及时性要求高的数据查询。

db.Person.Where(a => a.IsDeleted == false).AsNoTracking().ToList(); 

下面是本人编写关于更改AsNoTracking数据Update的两种方式测试与总结:

EntityDB db = new EntityDB(); var users = db.User.AsNoTracking().ToList(); foreach (var user in users) { db.Set<User>().Attach(user); } foreach (var user in users) { user.IsDeleted = true; //db.Entry(user).State=EntityState.Modified; } db.SaveChanges(); 

以上代码我将未跟踪的数据作Attach后赋值SaveChanges生成的SQL语句以下:

enter description here

而采用直接赋值后Entry修改State状态为Modified

EntityDB db = new EntityDB(); var users = db.User.AsNoTracking().ToList(); /* foreach (var user in users) { db.Set<User>().Attach(user); }*/ foreach (var user in users) { user.IsDeleted = false; db.Entry(user).State=EntityState.Modified; } db.SaveChanges(); 

生成的SQL语句以下:

enter description here

对比咱们得出结论第一种采用Attach后赋值的方法是执行的按需更新,也就是说更新哪一个字段就update它,而第二种则是无论更新了哪一个字段,生成的SQL语句都是更新所有。

为何第一种方法中我Attach后仅仅只是给对象赋值且没有修改State为Modified,但EF却能帮我修改数据值,那是由于

当SaveChanges时,将会自动调用DetectChanges方法,此方法将扫描上下文中全部实体,

并比较当前属性值和存储在快照中的原始属性值。若是被找到的属性值发生了改变,

此时EF将会与数据库进行交互,进行数据更新,因此不用设置State为Modified。

对于删除操做则须要在Attach后设置 db.Entry(user).State = EntityState.Deleted;

借鉴于此,我又封装了一个独立的AttachList方法,此方法仅仅只是将由AsNoTracking 取得的数据附加到上下文中,由于不用关注以后的操做是Update或者Delete因此只用了Attach。

如下截图代码是直接从个人项目中摘取出来展现:

enter description here

其中最关键的是性能上的提升(就是上述文字标记的地方),当查询大量数据时,使用此方法比不使用而将其附加到上下文容器中,性能提高不是一点点。

6.EF使用SqlQuery

对于某些特殊业务,咱们也可使用sql语句查询实体,如下只是一个简单的事例操做

SqlParameter[] parameter = { };
var user = db.Database.SqlQuery<User>("select * from user", parameter).ToList(); 

此方法得到的实体查询是在数据库(Database)上,实体不会被上下文跟踪。

SqlParameter[] parameter = { }; var user = db.Set<User>().SqlQuery("select * from user", parameter).ToList(); 

此方法得到的实体查询是被上下文跟踪,因此能直接赋值后SaveChanges()。

var user = db.Set<User>().SqlQuery("select * from user").ToList(); user.Last().Name = "makmong"; db.SaveChanges(); 

固然一样支持带参数的查询与存储过程操做,我就不一一列出了此处只作点出便可。

7.关于AsNonUnicode

咱们执行以下语句

var query = db.User.Where(a=>a.Name=="makmong").ToList(); 

生成的SQL语句

enter description here

再试一个语句

var query = db.User.Where(a=>a.Name== DbFunctions.AsNonUnicode("makmong")).ToList(); 

生成的SQL语句

enter description here

其中生成的SQL语句区别了,一个加了N,一个未加N,N是将字符串做为Unicode格式进行存储。

由于.Net字符串是Unicode格式,在上述SQL的Where子句中当一侧有N型而另外一侧没有N型时,此时会进行数据转换,也就是说若是你在表中创建了索引此时会失效代替的是形成全表扫描。

用 DbFunctions.AsNonUnicode 方法来告诉.Net将其做为一个非Unicode来对待,此时生成的SQL语句两侧都没有N型,就不会进行更多的数据转换,也就是说不会形成更多的全表扫描。

因此当有大量数据时若是不进行转换会形成意想不到的结果。

所以在进行字符串查找或者比较时建议用AsNonUnicode()方法来提升查询性能。

8.建议使用ViewModel代替实体Model

你们可能都会碰到这种状况就是Model实体拥有多个字段,可是查询数据到页面展现的时候可能只须要显示那么几个字段,这个时候建议使用ViewModel查询,

也就是说须要哪些字段就查询哪些,而不是 “select *”将所有字段加载出来。此操做即出于安全考虑 (不该该将实体Model直接传递到View上面),同时查询的字段减小 (可能就几个) 对查询性能也有所提高。

例:

var query = db.User.ToList(); 

对应的查询语句为:

enter description here

接着新建ViewModel

public class UserViewModel { public int Id { get; set; } public string Name { get; set; } } 

开始查询:

var query = db.User.Select(a=>new UserViewModel() { Id = a.Id, Name = a.Name }).ToList(); 

对应的查询语句为:

enter description here

9.建议Model实体中枚举使用byte类型

咱们先来了解下Sqlserver中tinyint, smallint, int, bigint的区别

  • bigint:从-263(-9223372036854775808)到263-1(9223372036854775807)的整型数据,存储大小为 8 个字节。一个字节就是8位,那么bigint就有64位

  • int:从-231(-2,147,483,648)到231-1(2,147,483,647)的整型数据,存储大小为 4 个字节。int类型,最大能够存储32位的数据

  • smallint:从-215(-32,768)到215-1(32,767)的整数数据,存储大小为 2 个字节。smallint就是有16位

tinyint:从0到255的整数数据,存储大小为 1 字节。tinyint就有8位。

因此对于有些范围比较短的数值长度,例如枚举类型值,彻底可使用byte类型替换int类型,对应生成数据库tinyint类型以节省数据存储。

如:

public CouponType CouponType { get; set; }
public enum CouponType : byte {  RedBag = 0,  Experience = 1,  Cash = 2,  JiaXiQuan = 3 } 

对应的数据库类型:

enter description here

此时的CouponType字段对应数据库就是一个tinyint类型

10.Model实体使用DateTime2替换DateTime控制内容值精度

咱们先看下 SQL Server中DateTime与DateTime2的区别

  • DateTime字段类型对应的时间格式是 yyyy-MM-dd HH:mm:ss.fff ,3个f,精确到1毫秒(ms),示例 2014-12-03 17:06:15.433 。

  • DateTime2字段类型对应的时间格式是 yyyy-MM-dd HH:mm:ss.fffffff ,7个f,精确到0.1微秒(μs),示例 2014-12-03 17:23:19.2880929 。

咱们知道EF Model的DateTime对应的SQL类型是DateTime

例:

public DateTime CreateDateTime { get; set; } 

对应的数据库实体类型:

enter description here

enter description here

可是在业务操做中不少时间值咱们仅仅只须要精确到秒就够了(特殊业务除外),

那多余的毫秒数既无用又占数据库存储(逼死处女座),既然是优化操做那么咱们是否能够去除毫秒数而只存储到秒呢?例:2014-12-03 17:06:15

So咱们可使用特性Attribute及抽象类PrimitivePropertyAttributeConfigurationConvention来达到这一目的。

很少说直接上代码:

[AttributeUsage(AttributeTargets.Property)]
public sealed class DateTime2PrecisionAttribute : Attribute { public DateTime2PrecisionAttribute(byte precision = 0) { Precision = precision; } public byte Precision { get; set; } } 
public class DateTime2PrecisionAttributeConvention: PrimitivePropertyAttributeConfigurationConvention<DateTime2PrecisionAttribute> { public override void Apply(ConventionPrimitivePropertyConfiguration configuration, DateTime2PrecisionAttribute attribute) { if (attribute.Precision > 7) { throw new InvalidOperationException("Precision must be between 0 and 7."); } configuration.HasPrecision(attribute.Precision); configuration.HasColumnType("datetime2"); } } 

理解一下代码,第一句中的AttributeTargets.Property表示能够对属性(Property)应用特性(Attribute)

而构造函数DateTime2PrecisionAttribute则指定了要应用的datetime的精度值。

而最后两句

configuration.HasPrecision(attribute.Precision); configuration.HasColumnType("datetime2"); 

则是将咱们所定义的类型精度与对应声明数据类型附加给要标记的实体类型。

最后还须要将DateTime2PrecisionAttributeConvention方法注册到咱们的DbContext中

public virtual DbSet<User> User { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Add(new DateTime2PrecisionAttributeConvention()); } 

如今咱们再使用此特性在上面的属性CreateDateTime中看下效果吧

结果图:

enter description here

enter description here

是否是感受不错。固然基于此拓展,咱们也能够扩展咱们想要的Model数据类型,如:控制decimal的精度(2位或4位小数),改边nvarchar(max)为咱们想要的长度类型(具体状况看业务再优化吧)。

11.合理使用EF扩展库

1.EF实现指定字段的更新

在以往的数据更新操做中咱们使用EF的修改都是先查询一次数据附加到上下文中,而后给须要修改的属性赋值,虽然说EF可以自动跟踪实体作到按需更新,但更新前查询不只没有必要,并且增长了额外的开销。EF删除和修改数据只能先从数据库取出,而后再进行删除.

当进行以下操做时:

delete from user where Id>5; update user set Name=”10”; 

咱们须要这样操做

var t1 = db.User.Where(t => t.Id > 5).ToList(); foreach (var t in t1) { db.User.Remove(t); } db.SaveChanges(); var t2 = db.User.ToList(); foreach (var t in t1) { t.Name = "ceshi"; } db.SaveChanges(); 

有没办法作到一条语句操做的更改呢?如“update user set name=’张三’where id=1”。

此时就须要使用EF的扩展库EntityFramework.Extended了。

在github中提供了一个EF扩展库https://github.com/loresoft/EntityFramework.Extended

在VS能够直接经过NuGet安装

enter description here

安装完成后试验下:

固然须要先引用:

using EntityFramework.Extensions; 

编写代码测试及查看结果:

EntityDB db = new EntityDB(); db.User.Where(a => true).Update(a => new User() {Name = "ceshi"}); 

enter description here

EntityDB db = new EntityDB(); db.User.Where(a => true).Delete(); 

enter description here

嗯,至于具体选择怎么用,看业务分析哈。

2.批量查询功能

例如:在分页查询的时候,须要查询结果数,和结果集

EF作法:查询两次

var q = db.User.Where(u => u.Name.StartsWith("a")); var count = q.Count(); var data = q.Skip(10).Take(10).ToList(); 

EF扩展库的作法:一次查询

var q = db.User.Where(t => t.Name.StartsWith("a")); var q1 = q.FutureCount(); var q2 = q.Skip(10).Take(10).Future(); var data = q2.ToList(); var count = q1.Value; 

3.查询缓存功能

咱们如今的后台项目权限管理模块,全部的菜单项都是写进数据库里,不一样的角色用户所获取展现的菜单项各不相同。

项目导航菜单就是频繁的访问数据库致使性能低下(一开始获得1级菜单,而后经过1级获取2级菜单,2级获取3级)

解决方法就是第一次查询后把数据给缓存起来设定缓存时间,而后一段时间继续查询此数据(譬如整个页面刷新)则直接在缓存中获取,从而减小与数据库的交互。

代码以下:

var users = db.User.Where(u => u.Id > 5).FromCache(CachePolicy.WithDurationExpiration(TimeSpan.FromSeconds(30))); 

若是在30秒内重复查询,则会从缓存中读取,不会查询数据库

咱们再提出二个问题那就是,

1:第一次查询缓存数据修改后(如:保存到数据库)紧接着继续查询一次,因为缓存时间没有失效,此时在缓存中查询的数据是刚刚修改的最新的吗?

2:在不一样的上下文中缓存获取结果是同样的吗?

写代码测试看下:

enter description here

上图中我在第一个上下文中得到数据缓存,而后给Name赋值”sss”,固然此处为了测试缓存是否更新因此我没有作SaveChanges()的操做,而后接着从缓存中获取数据,由结果可知此缓存值也相应的更改了。

所以在一段时间内即便操做修改了数据值也只须要在更改的时候操做一次数据库,减小了与数据库的交互。

另外须要注意的是更改的时候能够根据操做结果选择是否继续缓存,例如数据更改失败可是缓存却改动了,下次取值数据就会不一致,因此当咱们在更新数据库失败时就能够选择移除缓存调用RemoveCache()方法。

12.EF使用SQL分库操做

当数据库的表及数据达到必定规模后咱们想到的优化就有分库,分表之类的优化操做。

对于以前的ADO.NET来讲分库是一件很普通的操做。

好比下面的非跨数据库查询语句:

SELECT Name FROM dbo.User WHERE ID=1 

跨数据库查询语句:

SELECT Name FROM MaiMangAdb.dbo.blog_PostBody WHERE ID=1 

咱们知道EF的DbContext中已经指定了链接字符串

public EntityDB() : base("DefaultConnection") 
<connectionStrings> <add name="DefaultConnection" connectionString="Data Source=.;Initial Catalog=EFStudy;Integrated Security=True;" providerName="System.Data.SqlClient" /> </connectionStrings> 

也就是说全部的上下文操做都是基于这个数据库来操做的,那咱们就不能用ADO.NET那套,多个查询配多个连接去操做数据库。

固然大神们也给出了一套方法,并且也是简单明了。那我也就直接将其移植过来记录一下吧。

方法就是给数据库添加SYNONYM 同义词,我在此演示下

建立2张Model表User和Role

public class User { [Key] public int Id { get; set; } public string Name { get; set; } public bool IsDeleted { get; set; } [DateTime2Precision] public DateTime CreateDateTime { get; set; } } public class Role { [Key] public int Id { get; set; } public string Name { get; set; } } 

并添加一条语句:

EntityDB db = new EntityDB(); db.User.Add(new User { Id = 1, Name = "ddd" ,CreateDateTime = DateTime.Now}); db.Role.Add(new Role() {Id = 1, Name = "admin"}); db.SaveChanges(); 

运行查看数据库:

enter description here enter description here

如今数据库表及内容都有了。而后我要把User表及内容移植到另外一个数据库中,且不影响当前的EF操做。

建立新的数据库EFSYNONYM并添加User表,表结构和EFStudy中的User一致。

image

而后在EFStudy中删除表User且建立同义词

CREATE SYNONYM [dbo].[Users] FOR [EFSYNONYM].[dbo].[Users] 

效果如图

enter description here

此时的User和Role已经分别存在于不一样的数据库里面,咱们来插入查询数据操做下

enter description here

至此分库成功。固然此方法也有个缺点就是分库表和主表间由同义词关联而没法创建主外键关系(其实当数据量达到必定级别后联合join查询反而不如分开屡次查询来得快,

且因为在同一个上下文中,不用太过于关心由数据屡次链接开关而产生影响,凡事有利弊总得有个最优是吧),所以咱们能够把一些独立的容易过时的数据表给移植到单独的数据库,利于管理同时也利于优化查询。

相关文章
相关标签/搜索