EntityFramework之原始查询及性能优化(六)

前言

在EF中咱们能够经过Linq来操做实体类,可是有些时候咱们必须经过原始sql语句或者存储过程来进行查询数据库,因此咱们能够经过EF Code First来实现,可是SQL语句和存储过程没法进行映射,因而咱们只能手动经过上下文中的SqlQuery和ExecuteSqlCommand来完成。sql

SqlQuery

sql语句查询实体

 经过DbSet中的SqlQuery方法来写原始sql语句返回实体实例,若是是经过Linq查询返回的那么返回的对象将被上下文(context)所跟踪。数据库

首先给出要操做的Student(学生类),对于其映射这里再也不叙述,本节只讲查询。express

public class Student { public int ID { get; set; } public string Name { get; set; } public int Age { get; set; } }

若是咱们要查询学生表(Student)全部数据应该如何操做呢?下面咱们经过代码来进行演示:性能优化

EntityDbContext ctx = new EntityDbContext(); SqlParameter[] parameter = { }; ctx.Database.SqlQuery<Student>("select * from student", parameter).ToList();

咱们经过Sql Server Profiler监控其执行语句以下图,达到预期所想。框架

【注意1】上述我标注 实体实例 为红色的地方,返回的必须是一个实体即全部列,若是有些列未返回将报错!假设咱们只查出学生表中Age和Name,咱们这样写查看语句ide

ctx.Database.SqlQuery<Student>("select Name, Age from Student").ToList();

这样将会报错以下:性能

【注意2】上述我标注了 ToList() 为红色的地方,正如上述所说Linq查询同样,这个查询语句直到结果所有被枚举完也就是ToList()以后才会执行。测试

那问题来了,接下来咱们进行以下操做,数据库会进行相应的修改?优化

 var entity = ctx.Database.SqlQuery<Student>("select * from student").ToList(); entity.Last().Name = "0928"; ctx.SaveChanges();

咱们查询出数据,并将其最后一条数据为xpy0928的修改成0928。结果以下:ui

显示并未进行修改,那咱们接着进行以下操做,又会如何呢?

var entity = ctx.Set<Student>().SqlQuery("select * from student").ToList(); entity.Last().Name = "0928"; ctx.SaveChanges();

 结果以下,显示进行了相应的改变:

因此基于此咱们得出结论:

ctx.Database.SqlQuery<TEntity>():SqlQuery方法得到的实体查询是在数据库(Database)上,实体不会被上下文跟踪。

ctx.Set<TEntity>().SqlQuery():SqlQuery方法得到实体查询在上下文中的实体集合上(DbSet)上,实体会被上下文跟踪。

那么问题来了,若是要是有参数的话该如何进行查询呢?

例如:要查询Name="xpy0928"和Age=5的学生该如何查询呢?下面咱们一步一步来进行尝试和操做

var Name = "xpy0928"; var Age = 5; var sql = "select Name, Age from Student where Name = @Name and Age = @Age"; ctx.Database.SqlQuery<Student>(sql, Name, Age).ToList();

咱们运行看看,结果出错以下:

先无论错误,咱们进行第二次尝试:

 var Name = "xpy0928"; var Age = 5; var sql = "select ID, Name, Age from Student where Name = {0} and Age = {1}"; ctx.Database.SqlQuery<Student>(sql, Name, Age).FirstOrDefault();

结果查询正常进行,未出错,从下面监控中能够看到:

 从出错的上面那个到这个正常运行的相信你看到区别了,我也已进行红色标记,既然上面的参数@符号很差使,咱们用SqlParameter试试看:

var Name = "xpy0928"; var Age = 5; var sql = "select ID, Name, Age from Student where Name = @Name and Age = @Age";
ctx.Database.SqlQuery
<Student>( sql, new SqlParameter("@Name", Name), new SqlParameter("@Age", Age));

结果运行正确,因此第一种出现的错误就是由于未使用SqlParameter,而该SqlParameter是继承自DbContext中的DbParameter经过下图能够看出:

至此咱们总结出进行查询的两种方式:

经过使用参数如{0}语法来实现

经过使用DbParameter子类而且使用@ParamateName语法来实现

 

 sql语句查询非实体类型

经过sql语句咱们能返回任意类型的实例包括类型!假设咱们只查出学生表中(某一列)全部学生的Age(年龄),咱们经过SqlQuery方法这样作:

ctx.Database.SqlQuery<int>("select Age from Student").ToList();

 咱们经过快速监视查到返回Age的集合以下,如咱们所指望:

 从上述你是否是发现EF经过sql查询和ADO.NET查询数据库没什么区别呢?no,远不止于此,请继续往下看!

*经过存储过程加载实体

咱们能够加载实体经过存储过程得到的结果。例如:咱们得到全部的学生列表,能够进行以下操做:

ctx.Database.SqlQuery<Student>("dbo.GetList").ToList();

如此将执行数据库中名为 GetList() 的存储过程,就是这么简单!彷佛没什么特别的,你会想还不如用sql语句查询了,其实远不止于此,上述给的例子是无参数,若是咱们须要参数呢?假设咱们要得到Age(年龄)等于5的全部人的姓名和年龄,那么该如何实现呢?

 咱们一步一步实现:

先建立要调用的存储过程GetList

CREATE PROCEDURE [dbo].[GetList] @Age INT AS BEGIN SELECT ID, Name, Age FROM dbo.Student WHERE Age = @Age END

/*查询出的全部列必须对应返回实体中的全部字段,缺一不可,不然报错*/

EF上下文调用存储过程:

var param = new SqlParameter("Age", 5); var list = ctx.Database.SqlQuery<Student>("dbo.GetList @Age", param).ToList();

运行结果如预期同样!【注意】在调用存储过程当中,若是数据库是Sql 2005要在存储过程名称前加上 EXEC  ,不然报错。

那么问题又来了,若是要输出参数的值,那么该如何操做呢? 

假设要经过学生名字(Name)来进行分页,此时还要得到数据总条数。因而咱们进行下面操做:

第一步:建立要调用存储过程

CREATE PROCEDURE [dbo].[Myproc] @Name NVARCHAR(max), @PageIndex int, @PageSize INT, @TotalCount int OUTPUT as declare @startRow int declare @endRow int
    
    set @startRow = (@PageIndex - 1) * @PageSize + 1
    set @endRow = @startRow + @PageSize - 1
    
    select * FROM ( select top (@endRow) ID, Age, Name, row_number() over(order by [ID] desc) as [RowIndex] from dbo.Student ) as T where [RowIndex] >= @startRow AND T.Name = @Name SET @TotalCount=(select count(1) as N FROM dbo.Student WHERE Name = @Name)

EF上下文调用存储过程:

var name = new SqlParameter { ParameterName = "Name", Value = Name }; var currentpage = new SqlParameter { ParameterName = "PageIndex", Value = currentPage }; var pagesize = new SqlParameter { ParameterName = "PageSize", Value = pageSize }; var totalcount = new SqlParameter { ParameterName = "TotalCount", Value = 0, Direction = ParameterDirection.Output }; var list = ctx.Database.SqlQuery<Student>("Myproc @Name, @PageIndex, @PageSize, @TotalCount output", name, currentpage, pagesize, totalcount); totalCount = (int)totalcount.Value;  /*得到要输出参数totalcount的值*/

【注意】此时要在要输出的输出参数标记为output。见如图红色标记。

那么问题来了,当经过存储过程查询大量数据时,此时查询出的数据未进行跟踪(由上已知),由于咱们要进行后续如删除之类的操做,因此要EF上下文来进行跟踪,咱们应该如何操做来提高最大的性能呢?

咱们能够对存储过程进行封装,而且能够简化调用存储过程同时提升查询的性能,请看以下:

public IList<TEntity> ExecuteStoredProcedureList<TEntity>(string commandText, params object[] parameters) where TEntity : class { if (parameters != null && parameters.Length > 0) { for (int i = 0; i <= parameters.Length - 1; i++) { var p = parameters[i] as DbParameter; if (p == null) throw new Exception("Not support parameter type"); commandText += i == 0 ? " " : ", "; commandText += "@" + p.ParameterName; if (p.Direction == ParameterDirection.InputOutput || p.Direction == ParameterDirection.Output) { commandText += " output"; } } } var result = this.Database.SqlQuery<TEntity>(commandText, parameters).ToList(); bool acd = this.Configuration.AutoDetectChangesEnabled; try { this.Configuration.AutoDetectChangesEnabled = false; for (int i = 0; i < result.Count; i++) result[i] = this.Set<TEntity>().Attach(result[i]); } finally { this.Configuration.AutoDetectChangesEnabled = acd; } return result; }

此时存储过程名称后面就无需继续填写存储过程当中如@参数了,调用以下:

var list = ctx.ExecuteStoredProcedureList<Student>("Myproc", pageindex, pagesize, totalcount);

只是作了个简化而已,最关键的是性能上的提升(就是上述红色标记的地方,若是不明白能够参考我有关【我为EF正名】这篇文章),作了下实际测试,当查询10000条数据时,若是不用红色标记,直接将其附加到上下文容器中,则须要以下时间(单位是毫秒)

当添加后,只需以下时间:

第一个和第二个咱们分别按照399秒和3秒来算的话,也就是133倍,可想而知,咱们仅仅只是一个小的操做,就达到如此大的性能的提高。经过实际测验,若是你如今还担忧EF性能的问题,那我也默默无语了,只要你恰当的运用而不是滥用一通。

对于SqlQuery不管是实体仍是非实体抑或存储过程查询都存在必定的局限性。由于很容易会出现数据读取器与指定的实体类不兼容,该类型中缺乏的成员在同名的数据读取器中没有对应的列,也就是说必须查出该实体中全部字段即映射到数据库中全部列。

非查询命令ExecuteSqlCommand 

该查询主要是针对非查询的命令如删除(delete) 、修改(update)等,其操做方式和上述SqlQuery同样。

【注意】用此方法对数据库做出的任何的更改,直到实体从数据库中被加载或从新加载,不然此更改对于EF上下文是不透明的。

SqlQuery和ExecuteSqlCommand方法主要区别:SqlQuery返回实体数据或者集合数据,而ExecuteSqlCommand是非查询命令,因此只是返回删除(delete)和更新(update)以及插入(insert)是否成功或者失败的状态码。

为何要使用DbContext而不使用ObjectContext

DbContext是比较新的API,它其中简单的API被设计的是如此的巧妙,对于开发者来讲无疑是一次全新的体验,可是若是你想要使用更加复杂的特性时,这时你不得不从DbContext中来得到ObjectContext而且使用旧的API。而且ADO.NET团队也建议使用愈来愈受欢迎的DbContext。

 

EF 4.x生成器建立了更多复杂的类,可是在内部其利用了关系修正,可是此特性却被证实当和延迟加载一块儿使用时倒是至关的低效,因此新的DbContext生成器再也不使用那。

因此基于上述描述,ObjectContext未被彻底抛弃,它们彻底是能够相互进行转换的。

所以在代码上从一个API到另外一个的转换也是彻底支持的。

(1)db=>ob(经过IObjectContextAdapter中的Adapter从DbContext迁移至ObjectContext)

var context = ((IObjectContextAdapter)ctx).ObjectContext;

(2)ob=>db(经过DbContext的构造器中的ObjectContext来建立一个新的DbContext上下文实例)

 ObjectContext ob; var context = new DbContext(ob, true);

例如在EF 4.x版本中的ObjectContext中使用编译查询(CompiledQuery)来提升查询性能(由于在Linq To Entity使用Linq,EF须要解析表达式树并将其转换为SQL,因此当须要屡次查询时可使用编译查询来保存输出),该编译查询不兼容DbContext。以下:

Func<EntityDbContext,string,IQueryable<Student>> query= 
CompiledQuery.Compile<EntityDbContext,string,IQueryable<Student>> ((EntityDbContext ctx,string property)=> from o in ctx.Set<Student>().ToList() where o.Name == property select o ); foreach (var item in query(EntityDbContext,"xpy0928") { Console.WriteLine(item.Name); }

固然使用编译查询也有诸多限制,好比说此查询执行至少不止一次,而且仅仅是参数不一样而已等等。

性能优化

(1)AsNoTracking

前几篇文章也已涉及到关于变动追踪的问题,若是当从数据库查出数据后并对其数据进行相应的更改,此时能够经过局部关闭变动追踪以及手动更改其状态达到一点点小小的优化。以下:

var list = ctx.Set<Student>().AsNoTracking().ToList(); var entity = list.Last(d => d.Name == "0928"); ctx.Set<Student>().Attach(entity); entity.Name = "xpy0928"; ctx.Entry(entity).State = EntityState.Modified; ctx.SaveChanges();

/*
先关闭追踪,而后对其实体数据进行修改,而后将其附加到上下文容器中使其被追踪,接着更改其为修改状态,最后调用SaveChanges检测其已被修改,更新数据到数据库 */

(2)AsNonUnicode

 咱们执行以下语句,并用SqlProfiler监控其SQL:

var query = ctx.Set<Student>().Where(d => d.Name == “Recluse_Xpy”).ToList();

生成的SQL语句以下:

接下来咱们这样操做,再看看生成的SQL语句:

 var query = ctx.Set<Student>().Where(d => d.Name == EntityFunctions.AsNonUnicode("Recluse_Xpy")).ToList();

其生成的SQL语句以下:

相信你也看出其中生成的SQL语句区别了,一个加了N,一个未加N,都知道N是将字符串做为Unicode格式进行存储。由于.Net字符串是Unicode格式,在上述SQL的Where子句中当一侧有N型而另外一侧没有N型时,此时会进行数据转换,也就是说若是你在表中创建了索引此时会失效代替的是形成全表扫描。用 EntityFunctions.AsNonUnicode 方法来告诉.Net 将其做为一个非Unicode来对待,此时生成的SQL语句两侧都没有N型,就不会进行更多的数据转换,也就是说不会形成更多的全表扫描。因此当有大量数据时若是不进行转换会形成意想不到的结果,所以在进行字符串查找或者比较时建议用AsNonUnicode()方法来提升查询性能。

*当心EF 6.1字符串尾随空格问题

当比较字符串时SQL Server会自动忽略空格,可是在.NET中尤为是在EF中却不会忽略空格,例如“1234   ”和“1234”在SQL Server中会被认为是相等的,可是在EF中由于关系修正却不会忽略空格。

对于上述问题咱们最好是经过一个示例来进行演示以此来加深理解并去解决它。

假设场景:一朵小红花(Flower)对应多个学生(Student),可是这个小红花确定只会被一个学生拿走也就只对应一个学生(两个类都用字符串做为主键)。鉴于此,咱们给出以下类,并给出相应的映射。

小红花类:

public class Flower { public string Id { get; set; } public string Remark { get; set; } public virtual ICollection<Student> Students { get; set; } }

学生类:

public class Student { public string Id { get; set; } public string Name { get; set; } public string FlowerId { get; set; } public virtual Flower Flower { get; set; } }

映射类:

 public class StudentMap : EntityTypeConfiguration<Student> { public StudentMap() { ToTable("Student"); HasKey(key => key.Id); HasRequired(p => p.Flower).WithMany(p => p.Students).HasForeignKey(p => p.FlowerId); } }

 接下来咱们插入数据进行测试:(在Flower类上的主键值Id有尾随空格可是在Student类的外键值FlowerId没有尾随空格)

ctx.Set<Flower>().Add(new Flower() {  Id = "flowerId ", Remark = "so bad" }); ctx.Set<Student>().Add(new Student() { Id = "xpy0928", FlowerId = "flowerId", Name = "xpy0928 study ef" }); ctx.Set<Student>().Add(new Student() { Id = "xpy0929", FlowerId = "flowerId", Name = "xpy0929 study ef" });

接着咱们进行打印插入的数据:

var flower = ctx.Set<Flower>().Include(p => p.Students).ToList(); Console.WriteLine("小花在内存中的数量" + ctx.Set<Flower>().Local.Count); Console.WriteLine("学生在内存中的数量" + ctx.Set<Student>().Local.Count); Console.WriteLine("学生在小花的外键导航属性的数量" + flower[0].Students.Count);

什么状况,结果告诉咱们出错了,以下:

此时咱们将上述红色标记尾随空格去掉,再进行测试,结果以下,如咱们预期同样:

出现有错误的结果就是咱们要说的问题。当咱们从数据库中查询插入的全部Student和Flower时,此时如咱们预期的同样,成功的返回了数据,由于数据库此时忽略上述红色标记的空格。可是在Flower上的导航属性Student却没有成功的被填充进来,由于EF不会忽略空格因此值也就没法进行匹配。咱们简单的将此问题进行描述以下

 EF实体框架在内存中的语义为【关系修正(Relationship FixUp)】,当进行匹配时,在关系修正的过程当中EF主要着眼于主键和外键的值以及填充导航属性,可是其就在处理字符串尾随空格的执行方式上与SQL Server不一样。

 既然问题已经很明显了,咱们接下来的工做就是去解决。以前系列文章中讲过监听者,咱们能够在查询以前利用监听者(或者说叫拦截者)来进行解决。

无需对数据库或者对现有的代码进行改造,在EF 6.1中利用监听者(拦截器)和公开构造的查询树来进行解决。

 下面是EF在查询以前进行操做来忽略尾随空格的代码

 public class EFConfiguration : DbConfiguration { public EFConfiguration() { AddInterceptor(new StringTrimmerInterceptor()); } } public class StringTrimmerInterceptor : IDbCommandTreeInterceptor { public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext) { if (interceptionContext.OriginalResult.DataSpace == DataSpace.SSpace) { var queryCommand = interceptionContext.Result as DbQueryCommandTree; if (queryCommand != null) { var newQuery = queryCommand.Query.Accept(new StringTrimmerQueryVisitor()); interceptionContext.Result = new DbQueryCommandTree( queryCommand.MetadataWorkspace, queryCommand.DataSpace, newQuery); } } } private class StringTrimmerQueryVisitor : DefaultExpressionVisitor { private static readonly string[] _typesToTrim = { "nvarchar", "varchar", "char", "nchar" }; public override DbExpression Visit(DbNewInstanceExpression expression) { var arguments = expression.Arguments.Select(a => { var propertyArg = a as DbPropertyExpression; if (propertyArg != null&& _typesToTrim.Contains(propertyArg.Property.TypeUsage.EdmType.Name)) { return EdmFunctions.Trim(a); } return a; }); return DbExpressionBuilder.New(expression.ResultType, arguments); } } }

上述就是对根表达树进行遍从来得到其属性,再将其含有字符串的除去尾随空格,而后让其监听者去执行咱们修改过的命令,最后只需将监听者(或者说是拦截器)进行注册便可。

结果运行正常:

补充:实体各个状态(EntityState)以及使用

EntityState 

  • Added:实体被上下文追踪,可是在数据库中不存在
  • Unchanged:实体被上下文追踪,在数据库中存在,而且从数据库中获取的属性值未发生改变
  • Modified:实体被上下文追踪,在数据库中存在,而且其部分或者所有属性值已经被修改
  • Deleted:实体被上下文追踪,在数据库中存在,可是当下一次SaveChanges被调用时,已经被标记为删除
  • Detached:实体不会被上下文追踪

添加(Add)一个实体到上下文

方法一

using (var context = new BloggingContext()) { var blog = new Blog { Name = "ADO.NET Blog" };  context.Blogs.Add(blog);  context.SaveChanges(); }

此实体经过DbSet中的Add方法被添加到上下文中,此时实体状态将为Added State,也就意味着当SaveChanges被调用的时候,该实体会插入到数据库中。

方法二

using (var context = new BloggingContext()) { var blog = new Blog { Name = "ADO.NET Blog" }; context.Entry(blog).State = EntityState.Added; context.SaveChanges(); }

直接经过Entry方法来设置其状态为Added状态。 

附加(Attach )已存在实体到上下文

若是一个实体老是存在数据库中,可是没有被上下文所追踪,因此此时须要经过DbSet上的Attach方法来跟踪该实体,而后该实体的状态为UnChanged。以下:

方法一

var existingBlog = new Blog { BlogId = 1, Name = "ADO.NET Blog" }; using (var context = new BloggingContext()) {  context.Blogs.Attach(existingBlog);  context.SaveChanges(); }

【注意】当调用SaveChanges时若是没有对实体作任何操做,此时数据库数据不会有任何改变,由于此时实体状态为UnChanged。

方法二

直接经过Entry方法来更改其状态为UnChanged

var existingBlog = new Blog { BlogId = 1, Name = "ADO.NET Blog" }; using (var context = new BloggingContext()) {  context.Entry(existingBlog).State = EntityState.Unchanged;  context.SaveChanges(); }

【注意】若是附加到上文容器中的实体的引用到了其余实体没有被追踪,那么此时这些新的实体将也会被附加到上下文中,而且其状态为UnChanged

附加(Attach)一个已存在可是修改的实体到上下文

若是一个实体老是存在数据库中而且此时对该实体做了相应的修改,那么此时应该修改它的状态为Modified

var existingBlog = new Blog { BlogId = 1, Name = "ADO.NET Blog" }; using (var context = new BloggingContext()) {  context.Entry(existingBlog).State = EntityState.Modified;  context.SaveChanges(); }

更改被追踪实体的状态 

若是一个实体一直被上下文所追踪,能够改变其状态经过Entry来设置状态属性。

var existingBlog = new Blog { BlogId = 1, Name = "ADO.NET Blog" }; using (var context = new BloggingContext()) {  context.Blogs.Attach(existingBlog); context.Entry(existingBlog).State = EntityState.Unchanged;  context.SaveChanges(); }

【注意】虽然Add和Attach方法是用来追踪一个实体,可是也能够被用来改变实体的状态。例如,上述经过调用Attach方法将当前处于Added状态的实体更改成UnChanged状态

相关文章
相关标签/搜索