EntityFramework之原始查詢及性能優化(六)


前言

在EF中我們可以通過Linq來操作實體類,但是有些時候我們必須通過原始sql語句或者存儲過程來進行查詢數據庫,所以我們可以通過EF Code First來實現,但是SQL語句和存儲過程無法進行映射,於是我們只能手動通過上下文中的SqlQuery和ExecuteSqlCommand來完成。

SqlQuery

sql語句查詢實體

 通過DbSet中的SqlQuery方法來寫原始sql語句返回實體實例,如果是通過Linq查詢返回的那么返回的對象將被上下文(context)所跟蹤。

首先給出要操作的Student(學生類),對於其映射這里不再敘述,本節只講查詢。

    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,我們這樣寫查看語句

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。結果如下:

顯示並未進行修改,那我們接着進行如下操作,又會如何呢?

                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();

/*
先關閉追蹤,然后對其實體數據進行修改,然后Attach附加到到上下文中,並通過Entry方法更改其為修改狀態,最后調用SaveChanges檢測其已被修改,更新數據到數據庫
*/

AsNoTracking補充

在此感謝園友流年莫逝,經其提示,上述當關閉追蹤時,進行修改后,無需利用Attach附加到上下文中,因為通過Entry方法就已經添加到上下文中(因為調用DetectChanges方法),所以上述 ctx.Set<Student>().Attach(entity); 是多此一舉,此句略去。后經過嘗試,總結如下:

當【修改】時無論是否關閉局部追蹤,都無需利用Attach來進行附加到上下文中只需通過Entry方法修改其為修改狀態(Modified)即可,但是當【刪除】時如果關閉局部追蹤,此時必須通過Attach來附加到上下文中並通過Entry方法標記為刪除狀態(Deleted),當然你也可以先將其標記為刪除狀態然后進行刪除即可,也無需附加,而如果未關閉局部追蹤則無需通過Attach附加到上下文只需通過Entry方法標記為刪除狀態(Deleted)。

若關閉局部追蹤並進行刪除未利用Attach附加到上下文中,此時報錯如下,,在ObjectStateManager里對象未被找到,所以無法檢測到對象。

(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()方法來提高查詢性能。

AsNonUnicode補充及注意

在此感謝園友筱伯,經其提示我也才發現 AsNonUnicode 已經被棄用,當敲代碼時查看其方法便有提示此方法已經被否決,現已用 DbFunctions 方法來代替,請注意!

*小心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狀態

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM