efcore技巧貼-也許有你不知道的使用技巧

前言

.net 環境近些年也算是穩步發展。在開發的過程當中,與數據庫打交道是必不可少的。早期的開發者都是DbHelper一擼到底,到如今的各類各樣的ORM框架大行其道。孰優孰劣誰也說不清楚,文無第一武無第二說的就是這個理。沒有什麼最好的,只有最適合你的。html

本人也是從DbHelper開始,期間用過SugarSql,再到EFCODE。本着學習分享的初衷分享本人工做中總結的一些小技巧,但願能幫助更多開發者,指望能達到共同進步。文中如有錯誤地方,歡迎你們不吝賜教。mysql

1. DbContext配置

在asp.net中,一般狀況下,經過在Startup類的ConfigureServices方法中,將ef服務注入。
示例代碼以下:算法

services.AddDbContext<DemoDbContext>(opt=>opt.UseMySql("server=.;Database=demo;Uid=root;Pwd=123;Port=3306;"));

以上代碼表示使用MySql數據庫。若是使用SqlServer數據庫,能夠把UseMySql改成UseSqlServer,其餘數據庫的使用方式也是經過調用不一樣的方法進行選擇。但須要安裝對應的擴展方法的程序包,如 Microsoft.EntityFrameworkCore.SqlServer 或 Microsoft.EntityFrameworkCore.Sqlite。sql

另外,UseMySql方法還包含了一個可空的Action 類型的參數,能夠經過此參數進行一些個性化的配置,好比配置重試機制。以下所示: docker

services.AddDbContext<DemoDbContext>(opt => opt.UseMySql("server=.;Database=demo;Uid=root;Pwd=123456;Port=3306;",
                provideropt => provideropt.EnableRetryOnFailure(3,TimeSpan.FromSeconds(10),new List<int>(){0} )));

這個重試機制在某些場景下仍是比較有用的。好比,因爲網絡波動或訪問量致使的一瞬間的鏈接超時。若是不設置重試機制,則會直接觸發異常,設置了超時後,則會根據設置的時間間隔以及重試次數進行重試。EnableRetryOnFailure方法的最後一個參數是用來設置錯誤代碼的,只有設置了錯誤代碼的錯誤,纔會觸發重試。獲取錯誤代碼的方法有不少種,我的比較推薦的是,經過異常信息進行獲取,好比,使用MySql數據時,觸發的異常類型是MySqlException,此類的Number屬性的值EnableRetryOnFailure方法所須要的Number數據庫

2. DbContext線程問題

efcore不支持在同一個DbContext實例上運行多個並行操做,這包括異步查詢的並行執行以及從多個線程進行的任何顯式併發使用。 所以,始終 await 異步調用,或對並行執行的操做使用單獨的 DbContext 實例。
當 EF Core 檢測到並行操做或多個線程同時嘗試使用 DbContext 實例時,你將看到一條 InvalidOperationException,其中包含相似於下面的消息:安全

A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.

意思是,在上一個操做沒有執行完畢以前,又啓動了一個新的操做,因此不能保證線程是安全的。服務器

下面是一段錯誤的,能夠觸發這個異常的示例代碼:網絡

因此,請始終await異步調用。若是在多個多個線程中使用DbContext,需保證每一個線程的DbContext的實例是惟一的。架構

3. 數據庫使用鏈接池

使用 services.AddDbContextPool比使用 services.AddDbContext吞吐量提高在10~20的百分點(非官方說法,對性能提升數據是本人測試後獲得的結果)。
須要注意的是,鏈接池大小並非越大越好。

4. 日誌記錄

在使用ef時,基本上絕大多數和數據庫的交互都是經過linq實現的,而後ef將linq翻譯成對應的sql語句,在排查問題的時候,在開發或者排查問題時,每每須要關注最終執行的sql腳本,因此就須要經過日誌的方式查看。
efcore2.x的版本默認是注入日誌服務,因此不須要額外的操做,就能夠查看對應的sql腳本。但efcore3.x的版本默認移除了日誌服務,具體緣由參照:https://docs.microsoft.com/zh-cn/ef/core/what-is-new/ef-core-3.0/breaking-changes#adddbc。
可經過自定義DbContext的方式注入日誌任務,示例代碼以下:

public static readonly ILoggerFactory MyLoggerFactory
            = LoggerFactory.Create(builder => { builder.AddConsole(); });
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    base.OnConfiguring(optionsBuilder);
    optionsBuilder.UseLoggerFactory(MyLoggerFactory);
}

當執行ef代碼時,可在控制檯中查看相關的sql腳本,以下圖所示:
TIM截圖20200618204603

5. 增

插入數據到數據庫經常使用的場景有:普通單表單行插入,多表級聯插入,批量插入。
普通單表單行插入比較簡單,實例代碼以下:

var student = new Student {CreateTime = DateTime.Now, Name = "zjjjjjj"};
await _context.Students.AddAsync(student);
await _context.SaveChangesAsync();

多表級聯插入,須要在實體映射中配置屬性導航。
好比Blog表和Post是的關係是1對多的關係。則在Blog的實體中,定義一個類型爲List 的屬性。示例代碼以下:

[Table("blog")]
public class Blog 
{
    [Column("id")]
    public long Id { get; set; }
    [Column("title")]
    public string Title { get; set; }
    public List<Post> Posts { get; set; }
    [Column("create_date")]
    public DateTime CreateDate { get; set; }
}

對應的插入語句以下所示:

var blog = new Blog
{
    Title = "測試標題",
    Posts = new List<Post>
    {
        new Post{Content = "評論1"},
        new Post{Content = "評論2"},
        new Post{Content = "評論3"},
    }
};
await _context.Blog.AddAsync(blog);
await _context.SaveChangesAsync();

執行此代碼,會生成以下的日誌:
111
從日誌中能夠看出,經過這種方式實現了級聯插入的效果。

批量插入實現方式有兩種,一種是EF默認實現,適用於數據源較少的狀況。另外一種,咱們基於EF開發一個大數據量批量插入的服務,適合於數據源大於1000的場景。在萬級及以上的數據量上,較EF默認的批量插入性能上有很是明顯的提高。具體參考:https://www.cnblogs.com/fulu/p/13370335.html

EF默認實現:

var list = new List<Student>();
for (int i = 0; i < num; i++)
{
    list.Add(new Student { CreateTime = DateTime.Now, Name = "zjjjjjj" });
}

await _context.Students.AddRangeAsync(list);
await _context.SaveChangesAsync();

ISqlBulk實現:

var list = new List<Student>();
for (int i = 0; i < 100000; i++)
{
    list.Add(new Student { CreateTime = DateTime.Now, Name = "zjjjjjj" });
}
await _bulk.InsertAsync(list);

自增 OR GUID

int自增的優勢:

一、須要很小的數據存儲空間,僅僅須要4 byte 。

二、insert和update操做時使用INT的性能比GUID好,因此使用int將會提升應用程序的性能。

三、index和Join 操做,int的性能最好。

四、容易記憶。

int自增的缺點:

一、使用INT數據範圍有限制。若是存在大量的數據,可能會超出INT的取值範圍。

二、很難處理分佈式存儲的數據表。

GUID作主鍵的優勢:

一、惟一性。

二、適合大量數據中的插入和更新操做。

三、跨服務器數據合併不是常方便。

GUID作主鍵的缺點:

一、存儲空間大(16 byte),所以它將會佔用更多的磁盤大小。

二、很難記憶。join操做性能比int要低。

三、沒有內置的函數獲取最新產生的guid主鍵。

四、EF默認生成的GUID是無序的,會影響數據插入性能。

結論:

在數據量比較少的場景下,建議使用int自增,好比分類。對於大數據量,建議使用有序GUID。由於默認.net生成GUID是無序的,而數據庫中主鍵默認是彙集索引,而彙集索引在物理上的存儲是有序的,當插入數據時,若是插入的是無序的GUID,可能就會涉及到移動數據的狀況,進而影響插入的性能,特別是百萬級數據量的時候,性能影響則較爲明顯。參考資料:https://www.cnblogs.com/CameronWu/p/guids-as-fast-primary-keys-under-multiple-database.html

其餘可選方案:

通過我的多番瞭解,目前市面上經常使用的分佈式id生成算法和Twitter發佈的雪花算法大同小異,我的也在項目中使用過雪花算法,有興趣的朋友能夠在博客園找下相關的內容。不過目前用.net封裝的雪花算法廣泛較基礎,很難在docker或者k8s環境下簡單的使用,因此在此預告下,本人根據雪花算法編寫的可用於k8s環境的即將開源,敬請期待。

6. 查

EF使用Linq查詢數據庫中的數據,使用Linq可編寫強類型的查詢。當命令執行時,EF先將Linq表達式轉換成sql腳本,而後再提交給數據庫執行。可在日誌中查看生成的sql腳本。

根據條件查詢:
await _context.Blog.Where(x=>x.Id>0).ToListAsync();

上述代碼執行時生成的sql腳本以下所示:

SELECT `x`.`id`, `x`.`create_date`, `x`.`title`
      FROM `blog` AS `x`
      WHERE `x`.`id` > 0
獲取單個實體

可實現獲取單個實體的方式有First,FirstOrDefault,Single,SingleOrDefault
其中First,FirstOrDefault執行時生成的sql腳本以下:

SELECT `x`.`id`, `x`.`create_date`, `x`.`title`
      FROM `blog` AS `x`
      WHERE `x`.`id` > 10
      LIMIT 1

Single,SingleOrDefault執行時生成的sql腳本以下:

SELECT `x`.`id`, `x`.`create_date`, `x`.`title`
      FROM `blog` AS `x`
      WHERE `x`.`id` > 10
      LIMIT 2

細心的你應該已經發現了二者的區別,Single須要查詢2條數據,當返回的數據多餘一條時,Single,SingleOrDefault方法就會報Source sequence contains more than one element.異常。因此Single方法僅適用於查詢條件對應的數據只有一條的場景,好比查詢主鍵的值。以下所示:

await _context.Blog.SingleOrDefaultAsync(x => x.Id==100);

後綴帶OrDefault和不帶後綴的區別是,當sql腳本執行查詢不到數據時,帶後綴的會返回空值,而不帶後綴的則會直接報異常。

判斷數據庫是否存在

可經過Any()和Count()方法實現是否存在數據。示例代碼以下:

await _context.Blog.AnyAsync(x => x.Id > 100);

await _context.Blog.CountAsync(x => x.Id > 100)>0;

生成的sql腳本對應以下:

SELECT CASE
          WHEN EXISTS (
              SELECT 1
              FROM `blog` AS `x`
              WHERE `x`.`id` > 100)
          THEN TRUE ELSE FALSE
      END
SELECT COUNT(*)
      FROM `blog` AS `x`
      WHERE `x`.`id` > 100

乍一看,Any方法生成的腳本貌似更復雜些,但實際上,Any方法的性能在大數據量下比Count方法高了不少。因此在判斷是否存在時,請使用Any方法。

鏈接查詢

鏈接查詢是關係數據庫中最主要的查詢,主要包括內鏈接、外鏈接(左鏈接、外鏈接)和交叉鏈接等。經過鏈接運算符能夠實現多個表查詢。本文主要講解下經常使用的內鏈接和左鏈接。
內鏈接的示例代碼以下:

var query = from post in _context.Post
            join blog in _context.Blog on post.BlogId equals blog.Id
    where blog.Id > 0
    select new {blog, post};

左鏈接的示例代碼以下:

var query = from post in _context.Post
                        join blog in _context.Blog on post.BlogId equals blog.Id
                        into pbs
                        from pb in pbs.DefaultIfEmpty()
                where pb.Id>0 && post.Content.Contains("1")
                        select new {post,pb.Title};
級聯查詢

在不少場景中,可能會涉及到查詢與父表關聯的子表數據,在這樣的場景中,會有一部分人先查出主表數據,而後根據主表的主鍵再去查詢子表的數據,筆者在使用ef初期也是這種處理方式的。但藉助Include的方法可讓咱們更方便的解決父子表級聯查詢的問題。示例代碼以下:

var result = await _context.Blog.Include(b => b.Posts) .SingleOrDefaultAsync(x=>x.Id==157);

若是有更多的層級,能夠藉助ThenInclude進行查詢。

有的時候,還有這樣的場景:咱們不是簡單的查詢子表的數據,而是須要查詢知足指定條件的數據,那就要求我們在調用Include的方法時傳入參數,示例代碼以下:

var filteredBlogs = await _context.Blogs
        .Include(blog => blog.Posts
            .Where(post => post.BlogId == 1)
            .OrderByDescending(post => post.Title)
            .Take(5))
        .ToListAsync();

注:以上方法僅在.net5中支持。因此,efcore也是在一個發展的過程當中,隨着時間與版本的更新,功能也會漸漸趨於完善。相關內容請參考:https://docs.microsoft.com/zh-cn/ef/core/querying/related-data

7. 改

使用過EF的應該都瞭解查詢的跟蹤與非跟蹤的概念吧(納尼?你沒據說過,老衲給您指條明路吧:https://docs.microsoft.com/zh-cn/ef/core/querying/tracking)。

一般來說,更新的流程大概是這樣:查詢出數據,修改某些字段的值,調用Update方法,而後調用SaveChange方法。看上去毫無破綻,但若是你仔細觀察過生成的sql腳本的話,或許你就應該有更好的方法,我們先來看看示例代碼:

var school = await _context.Schools.FirstAsync(x => x.Id > 0);
school.Name = "6666";
_context.Schools.Update(school);
await _context.SaveChangesAsync();

以下圖所示的是執行以上代碼生成的update的sql語句,咱們發現明明代碼中只對Name從新賦了值,但生成的腳本卻將此記錄的全部字段進行了更新,顯然這不是咱們想要的結果。

20200828011210

其實,若是實體是經過跟蹤查詢獲得的,則可直接調用SaveChage方法,而不用多餘調用Update方法,此時,EF內部會自動判斷哪些字段進行了更新,從而只生成值改變了的sql語句。

結論:當要更新的實體開啓了跟蹤,則更新時,無需調用Update方法, 直接調用SaveChange方法,此時以後更新值發生改變的字段。 若是先調用Update則SaveChange,則無論實體的字段有沒有更新,生成的sql腳本依舊會更新全部的字段,犧牲了性能。假如你的實體不是經過數據庫的跟蹤查詢獲取的,則在調用時才須要調用Update方法。


福祿ICH.架構出品

做者:福爾斯

2020年8月