EntityFramework 優化建議
原文地址 http://blog.jd-in.com/947.html
Entity Framework目前最新版本是6.1.3,當然Entity Framework 7 目前還是預覽版,並不能投入正式生產環境,估計正式版16年第一季度會出來,了解過EF7的部分新特性後,還是狠狠期待一下滴。
EF性能問題一直為開發者所詬病,最讓人糾結的也是這塊,所以此次我也來談談EF的性能優及建議。既然是把優化點列舉出來,可能有些地方關於底層的知識就不會介紹的太深刻,權當拋磚引玉吧。
先說說EF性能優化工具MiniProfiler,(不過也可以直接用Sqlserver profiler)MiniProfiler是StackOverFlow團隊設計的一款對.net的性能分析小程序。
在這裏我們可以使用MiniProfiler嵌入頁面查看頁面處理的周期和Sql語句執行的周期及Sql語句。可以通過Nuget下載MiniProfiler和MiniProfiler.EF然後進行安裝與配置(具體操作暫不細說)。
因為作為宇宙級的開發工具VS2015已經提供了一個更為直接明了的方式,那就是“診斷工具”,具體打開的位置
此工具能更為直觀的將EF操作數據庫的SQL語句所列舉出來。如我要查詢角色表數據
EntityDB db = new EntityDB();
db.Role.Where(a => a.Id > 1).Select(a => a.Id).ToList();
查看工具顯示
查看“執行Reader”可以看到SQL語句
方便你根據查詢語句修改你的查詢表達式及顯示model.
以下為此次目錄列表:
- 1.使用最新版的EF
- 2. 禁用延遲加載
- 3.使用貪婪加載(又叫預加載就是數據庫的多表查詢)
- 4.了解 IQueryable,IEnumerable的區別
- 5.優化操作AsNoTracking()與Attach
- 6.EF使用SqlQuery
- 7.關於AsNonUnicode
- 8.建議使用ViewModel代替實體Model
- 9.建議Model實體中枚舉使用byte類型
- 10.Model實體使用DateTime2替換DateTime控制內容值精度
- 11.合理使用EF擴展庫
- 1.EF實現指定字段的更新
- 2.批量查詢功能
- 3.查詢緩存功能
- 12.EF使用SQL分庫操作
下面開始一一介紹
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默認使用延遲加載獲取導航屬性關聯的數據。
作為默認配置的延遲加載,需要滿足以下幾個條件:
-
context.Configuration.ProxyCreationEnabled = true;
-
context.Configuration.LazyLoadingEnabled = true;
-
導航屬性被標記為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語句如下:
而采用直接賦值後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語句如下:
對比我們得出結論第一種采用Attach後賦值的方法是執行的按需更新,也就是說更新哪個字段就update它,而第二種則是不管更新了哪個字段,生成的SQL語句都是更新全部。
為什麽第一種方法中我Attach後僅僅只是給對象賦值且沒有修改State為Modified,但EF卻能幫我修改數據值,那是因為
當SaveChanges時,將會自動調用DetectChanges方法,此方法將掃描上下文中所有實體,
並比較當前屬性值和存儲在快照中的原始屬性值。如果被找到的屬性值發生了改變,
此時EF將會與數據庫進行交互,進行數據更新,所以不用設置State為Modified。
對於刪除操作則需要在Attach後設置 db.Entry(user).State = EntityState.Deleted;
借鑒於此,我又封裝了一個獨立的AttachList方法,此方法僅僅只是將由AsNoTracking 取得的數據附加到上下文中,因為不用關註之後的操作是Update或者Delete所以只用了Attach。
以下截圖代碼是直接從我的項目中摘取出來展示:
其中最關鍵的是性能上的提高(就是上述文字標記的地方),當查詢大量數據時,使用此方法比不使用而將其附加到上下文容器中,性能提升不是一點點。
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語句
再試一個語句
var query = db.User.Where(a=>a.Name== DbFunctions.AsNonUnicode("makmong")).ToList();
生成的SQL語句
其中生成的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();
對應的查詢語句為:
接著新建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();
對應的查詢語句為:
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
}
對應的數據庫類型:
此時的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; }
對應的數據庫實體類型:
但是在業務操作中很多時間值我們僅僅只需要精確到秒就夠了(特殊業務除外),
那多余的毫秒數既無用又占數據庫存儲(逼死處女座),既然是優化操作那麽我們是否可以去除毫秒數而只存儲到秒呢?例: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中看下效果吧
結果圖:
是不是感覺不錯。當然基於此拓展,我們也可以擴展我們想要的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安裝
安裝完成後試驗下:
當然需要先引用:
using EntityFramework.Extensions;
編寫代碼測試及查看結果:
EntityDB db = new EntityDB();
db.User.Where(a => true).Update(a => new User() {Name = "ceshi"});
EntityDB db = new EntityDB();
db.User.Where(a => true).Delete();
嗯,至於具體選擇怎麽用,看業務分析哈。
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:在不同的上下文中緩存獲取結果是一樣的嗎?
寫代碼測試看下:
上圖中我在第一個上下文中獲得數據緩存,然後給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();
運行查看數據庫:
現在數據庫表及內容都有了。然後我要把User表及內容移植到另一個數據庫中,且不影響當前的EF操作。
創建新的數據庫EFSYNONYM並添加User表,表結構和EFStudy中的User一致。
然後在EFStudy中刪除表User且創建同義詞
CREATE SYNONYM [dbo].[Users] FOR [EFSYNONYM].[dbo].[Users]
效果如圖
此時的User和Role已經分別存在於不同的數據庫裏面,我們來插入查詢數據操作下
至此分庫成功。當然此方法也有個缺點就是分庫表和主表間由同義詞關聯而無法建立主外鍵關系(其實當數據量達到一定級別後聯合join查詢反而不如分開多次查詢來得快,
且由於在同一個上下文中,不用太過於關心由數據多次連接開關而產生影響,凡事有利弊總得有個最優是吧),因此我們可以把一些獨立的容易過期的數據表給移植到單獨的數據庫,利於管理同時也利於優化查詢。
EntityFramework 優化建議