天天看點

EF聯合查詢,如何設定條件過濾從表資料

最近在使用EF進行聯合查詢過程中,遇到了一件不開心的事情。

已禁用懶加載
var post = await _repository.GetMyPostById(blogId, postId).AsNoTracking()
                        .Include(p => p.PostToCategories)
                        .SingleOrDefaultAsync();
           
如上代碼所示的查詢中,使用Include()關聯了PostToCategories,這是常用的聯合查詢方式。可是PostToCategories是軟删除(IsAcitve)的,使用Include()方法會把所有的相關的PostToCategories都查詢出來,這不是我們想要的結果。
首先分析下原因,Include()方法,是根據配置的關系查詢關聯的對象,是以我們隻要在它生成sql之前加上過濾條件就可以了,可是縱觀EF的api,無一能實作目的。既然這樣,我們隻能想想其他辦法了。
           
var post = await _repository.GetMyPostById(blogId, postId).AsNoTracking()
                        .Select( p => new {
                                                BlogPost = p,
                                                PostToCategories = p.PostToCategories.Where(c => c.IsActive)
                                                }).SingleOrDefaultAsync();
var blogPost = post.BlogPost;
blogPost.PostToCategories = post.PostToCategories.ToList();

           
用匿名類型接受主表和從表的查詢結果,這樣就可以為從表設定過濾條件了。最後,把匿名結果顯示指派給blogPost。
通過觀察生成的sql語句,我們發現确實是在 ` join PostToCategories ` 後面增加了where條件。
但是這種方法在面對多對多關系時,就**不夠優雅**了。
           
var post = await _repository.GetMyPostById(blogId, postId).AsNoTracking()
                        .Include(p => p.TagMap)
                        .Include(p => p.TagMap.Select(t => t.Tag))
                        .SingleOrDefaultAsync();
           

改寫成如下:

var post = await _repository.GetMyPostById(blogId, postId).AsNoTracking()
                        .Select( p => new {
                                                BlogPost = p,
                                                TagMap = p.TagMap,
                                                Tag = p.TagMap.Select(t => t.Tag),
                                                }).SingleOrDefaultAsync();
var blogPost = post.BlogPost;
blogPost.TagMap = post.TagMap;
blogPost.TagMap.ForEach(m => m.Tag = post.Tag.FirstOrDefault(g => g.Id == m.TagId));
           

是不是感覺不美好了?

那有沒有更好的辦法呢?stackoverflow上有人推薦了一個針對EF的擴充包 EntityFramework.DynamicFilters ,它的實作方式是在OnModelCreating的時候給Entity設定好過濾條件,目前DbContext對象涉及到該Entity類型的查詢時,都會自動加上過濾條件。

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Filter("PostToCategory_IsActive", (PostToCategory ptc) => ptc.IsActive, true);//為PostToCategory類型設定過濾條件:IsActive==true
}
           

直接使用Include()就可以得到我們想要的結果,如下所示:

var post = await _repository.GetMyPostById(blogId, postId).AsNoTracking()
                        .Include(p => p.PostToCategories)
                        .SingleOrDefaultAsync();
           

可是這樣以來所有的涉及到PostToCategories的查詢都會被過濾,怎麼辦呢?EntityFramework.DynamicFilters提供 禁用/啟動 過濾條件的API

context.DisableFilter/EnableFilter("PostToCategory_IsActive");// 在目前DbContext執行個體對象中禁用/啟用名為PostToCategory_IsActive的Filter

modelBuilder.DisableFilterGlobally("PostToCategory_IsActive");// 全局禁用名為PostToCategory_IsActive的Filter

context.DisableAllFilters();//禁用所有的Filters

context.EnableAllFilters(); //啟用所有的Filters

于是代碼就變成了這樣:

一、設定Filter

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Filter("PostToCategory_IsActive", (PostToCategory ptc) => ptc.IsActive, true);//為PostToCategory類型設定過濾條件:IsActive==true
}
           

二、全局禁用Filters

context.DisableAllFilters();
           

三、在需要過濾的地方啟用Filter

context.EnableFilter("PostToCategory_IsActive");
var post = await _repository.GetMyPostById(blogId, postId).AsNoTracking()
                        .Include(p => p.PostToCategories)
                        .SingleOrDefaultAsync();

           

而且還沒有考慮DbContext緩存查詢結果帶來的問題。

如果說第一種解決方案不優雅,那麼這種方案就是惡心。

--- 2016.10.10 ---

昨天借鑒了ABP的封裝思想,把EntityFramework.DynamicFilters的API封裝了下,确實美觀多了。

參見:

  • tkb至簡的博文
  • github

本來順風順水,如tkb至簡的博文中描述的那樣,優雅的過濾資料,可惜最後在一次儲存的時候出現了意外,最後發現原因出在和EntityFramework.Extended有沖突,因為後者是重新生成了SqlExpression,沒有對EntityFramework.DynamicFilters在設定where條件中的@DynamicFilterParam_000001指派。

暫時沒有想到更好的辦法,遂先滾回去。:(