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狀態

相關文章
相關標籤/搜索