1. 程式人生 > 程式設計 >C#表示式中的動態查詢詳解【譯】

C#表示式中的動態查詢詳解【譯】

前言

當您使用LINQ來處理資料庫時,這種體驗是一種神奇的體驗,對嗎?你把資料庫實體像一個普通的收集,使用Linq中像Where,Select或者 Take,這些簡單的使用就能讓程式碼可用了。

但是,讓我們考慮一下這裡是如何通過動態查詢和表示式樹實現此功能的:幕後發生的事情。您編寫的LINQ查詢將轉換為SQL(或其他方式),並將該SQL查詢傳送到資料庫。然後將資料庫的響應對映到C#物件。但是,如何完全轉換為SQL?

在本文中,您將看到諸如Entity Framework和MongoDB C#驅動程式之類的框架如何使用表示式樹進行轉換。您將看到如何親自使用表示式樹來構建動態查詢。這些查詢是您無法在編譯時建立的查詢,因為您將知道該查詢僅在執行時的外觀。

對可查詢樹和表示式樹進行揭祕

考慮以下使用Entity Framework 6的C#程式碼:

DbSet<Student> students = context.Students;
var billie = await students.Where(s => s.StudentName == "Billie").ToListAsync();

執行時,實體框架會產生以下SQL查詢:

SQL:SELECT
 [Extent1].[StudentID] AS [StudentID],[Extent1].[StudentName] AS [StudentName],[Extent1].[DateOfBirth] AS [DateOfBirth],FROM [dbo].[Students] AS [Extent1]
 WHERE N'Billie' = [Extent1].[StudentName]

請注意,WHERESQL查詢中有一個操作。那不是很明顯。如果SQL不包含WHERE,則所有學生都將從資料庫中帶走,並且篩選將在.NET程序中執行。實際上,以下程式碼可以做到這一點:

//BAD:
DbSet<Student> students = context.Students;
Func<Student,bool> predicate = s => s.StudentName == "Billie";
var x = students.Where(predicate).ToList();

在最後一個示例中,SQL查詢使所有學生進入流程,並將其對映到常規集合。不同之處在於,在第一段程式碼中,lambda是一個Expression<Func<Student,bool>>,它允許實體框架將其新增到SQL查詢中。在第二段程式碼中,lambda是a Func<Student,bool>,因此將Where執行操作符之前的所有操作並將其轉換為常規IEnumerable集合,然後執行其餘的查詢。

第二段程式碼在效能,記憶體和網路方面很糟糕。我們從網路中獲取了許多物件,而不是僅從資料庫中獲取一個專案。然後,我們使用CPU將它們序列化為C#物件。並用完記憶體將它們儲存在程序的堆中。

因此,讓我們回到第一段程式碼。如何await students.Where(s => s.StudentName == "Billie").ToListAsync()產生一個包含的SQL查詢WHERE N'Billie' = [Extent1].[StudentName]?

答案是表達樹。該程式碼s => s.StudentName == "Billie"實際上是一個結構化查詢,可以通過程式設計將其分解為節點樹。在此示例中,有6個節點。最頂層的節點是lambda表示式。左側是lambda引數。在它的右邊是Equal表示表示式的lambda主體。實體框架具有遍歷這些表示式樹並構造SQL查詢的演算法。其他資料來源提供程式(如Mongo DB C#驅動程式)也會發生同樣的事情,除了它會構造一個MongoDB json查詢。

C#表示式中的動態查詢詳解【譯】

C#表示式樹

在第一段程式碼中,型別s => s.StudentName == "Billie"為Expression<Func<Student,bool>>。這表示生成樹的表示式樹Func<Student,bool>。該Where子句接受這種型別的引數,因為aDbSet<TEntity>實現了IQueryable<T>介面,這要求它與表示式樹配合使用。相反,常規集合(如陣列或a List<T>)IEnumerable意味著它將lambdas => s.StudentName == "Billie"用作常規函式。

好的,但是我該如何利用它呢?

在大多數情況下,使用表達樹的人們就是在構建世界實體框架的人們。但是在某些特定情況下,它變得非常有用。這是我們最近在Ozcode[1](我的日常工作)中遇到的一個用例:

我們想在名為Error的資料庫實體上建立動態伺服器端過濾。該實體具有許多屬性,我們希望允許使用者對其進行過濾。因此過濾應根據被允許Username,Country,Version,或任何其他財產。這是我們需要實現的API:

IQueryable<Error> _errors; 
public IEnumerable<Error> GetErrors(string propertyToFilter,string value){ /*..*/} 

在這種情況下,propertyToFilter是的屬性Error。使用常規的LINQ,唯一的方法就是使用巨大的switch / case語句。有點像這樣:

IQueryable<Error> _errors;

public IEnumerable<Error> GetErrors(string propertyToFilter,string value)
{
 switch (propertyToFilter)
 {
  case "Username":
   return await _errors.Where(e=> e.Username == value).ToListAsync();
  case "Country":
   return await _errors.Where(e=> e.Country == value).ToListAsync();
  case "Version":
   return await _errors.Where(e=> e.Version == value).ToListAsync();
  // ...  
 }
}

您可能會同意這不是理想的選擇。除了必須編寫所有這些東西之外,它還非常容易出現錯誤。如果添加了屬性怎麼辦?如果重新命名怎麼辦?整個事情一團糟。

通過動態查詢和表示式樹可以實現此功能的方法如下:

private async static Task<IEnumerable<Error>> GetErrors(string propertyToFilter,string value)
{
 var error = Expression.Parameter(typeof(Error));
 var memberAccess = Expression.PropertyOrField(error,propertyToFilter);
 var exprRight = Expression.Constant(value);
 var equalExpr = Expression.Equal(memberAccess,exprRight);
 Expression<Func<Error,bool>> lambda = Expression.Lambda<Func<Error,bool>>(equalExpr,error);

 return await _errors.Where(lambda).ToListAsync();
}

這裡的每一行程式碼代表表示式樹中的一個節點。它們共同構成了最高節點-lambda。然後,可以在LINQ中使用動態表示式,並生成伺服器端SQL查詢。我認為很好。

解決此問題的另一種方法是構建自定義SQL查詢字串。在Ozcode中,我們使用的是MongoDB,因此SQL不適合使用,但我們可以建立一個自定義的MongoDB JSON查詢字串。也不是太難,但是我認為表示式樹方法更加靈活和可靠。一方面,您可以將其放在LINQ中並與其他LINQ運算子組合。此外,當有諸如Entity Framework之類的經過測試的框架可以為您執行此操作時,為什麼還要編寫自己的查詢。

概要

回顧一下。這是本文中的一些關鍵點:

•常規函式/委託與表示式之間的區別在於,表示式可以用結構化樹表示。可以輕鬆地分析該樹以建立諸如資料庫查詢之類的東西。

•支援表示式的資料來源實現該IQueryable介面。

•如果您無法使用表示式(以及使用常規方法或委託),則查詢將在伺服器端而不在資料庫端,這對於效能而言將是可怕的。

•使用lambda(不帶主體)時,表示式是無縫建立的,因此這些年來您可能一直都在這樣做。

•您可以自己使用表示式樹來建立動態查詢。這在無法在編譯時僅在執行時構建查詢的情況下很有用。

總結

到此這篇關於C#表示式中的動態查詢的文章就介紹到這了,更多相關C#表示式的動態查詢內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!

References

[1] Ozcode: https://oz-code.com

[2]: https://www.mediavine.com/