【EF Core】FromExpression 方法有什么用?
比 90% 的人細心的大伙伴一定發現了 DbContext 類有一個方法叫 FromExpression,它到底干嗎用的?官方文檔中沒有專門的介紹(只在表值函數映射的例子中看到)。
咱們先來看看此方法的簽名:
IQueryable<TResult> FromExpression<TResult>(Expression<Func<IQueryable<TResult>>> expression)
看著好像很復雜的樣子。其實不,咱們來拆解一下:
1、TResult 是類型參數(泛型的知識點沒忘吧),這里其實指的就是實體類型,比如,你的愛狗 Dog。
2、這個方法返回 IQueryable<T> 類型,說明允許你使用 LINQ 查詢。
3、重點理解其參數——Expression<TDelegate> 表達有個萬能規律:可以把與 TDelegate 類型兼容的 lambda 表達式直接賦值給 Expression<> 變量。即這個 FromExpression 方法可以使用以下 lambda 表達式作為參數:
() => [返回 IQueryable<T>]
這個委托的意思就是:它,單身狗(無參數)一枚,但可以生產 IQueryable<T> 對象。
哦,說了一大堆,還沒說這個方法到底有啥毛用。它的用處就是你可以指定一個表達式,讓 EF 一開始就返回篩選過的查詢。在 DbContext 的派生類中聲明 DbSet<T> 類型的帶 get + set 的公共屬性這種生成首個查詢(根查詢)是最常用的方案,這個相信大伙們都很熟了,EF Core 最基礎操作,老周就不多介紹了。假如你要對查詢的初始數據做篩選,那么,按照 DbSet 的方案,要先執行一下 SELECT **** FROM #$$#*& 語句,然后再執行 SELECT **** FROM &*^$ WHERE xxxxx,我還沒操作數據呢就執行了兩次 SELECT 語句了。所以說,如果你一開始并不打算提取所有數據,那么直接從一開始就執行 SELECT **** FROM xxxx WHERE yyyy 多好,何必多浪費一條 SQL 語句?
還有一種使用場景:你的數據不是從某個表 SELECT 出來的,而是從一個表值函數返回的,這種情況也要借助 FromExpression 方法。
不知道老周以上說明你是否明白?不明白沒關系,咱們實戰一下你就懂了。
咱們先定義的實體:
/// <summary> /// 妖書實體 /// </summary> public class Book { /// <summary> /// 標識 + 主鍵 /// </summary> public Guid BookId { get; set; } /// <summary> /// 書名 /// </summary> public string Title { get; set; } = string.Empty; /// <summary> /// 簡介 /// </summary> public string? Description { get; set; } /// <summary> /// 作者 /// </summary> public string Author { get; set; } = string.Empty; }
然后,繼承 DbContext 類,常規操作。
public class MyDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("Server=.\\TEST;Database=mydb;Integrated Security=True;Trust Server Certificate=True"); // 配置日志 //.LogTo(log => Debug.WriteLine(log)); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Book>(t => { t.ToTable("books", tb => { tb.Property(x => x.BookId).HasColumnName("book_id"); tb.Property(z=>z.Title).HasColumnName("title"); tb.Property(k => k.Description).HasColumnName("desc"); tb.Property(f => f.Author).HasColumnName("author"); }) .HasKey(t => t.BookId); }); } // 以下行現在不需要了 //public DbSet<Book> Books { get; set; } public IQueryable<Book> MyBooks => FromExpression(() => Set<Book>().Where(x => x.Author == "老周")); }
這里不用再定義 DbSet<> 類型的屬性了,因為我們要對數據進行篩選,重點看 MyBooks 屬性的實現:
public IQueryable<Book> MyBooks => FromExpression(() => Set<Book>().Where(x => x.Author == "老周"));
Set<Book>() 方法的調用會讓 DbContext 自動在緩存字典中添加數據集合,然后一句 Where 做出篩選,上述代碼的意思是只查詢老周寫的妖書,其他作者的不考慮。這時候 DbContext 不會發出 select * from xxx SQL 語句,所以你不用擔心執行多余的 SQL。調用 FromExpression 方法后,會使初始查詢直接生成 Select * from xxx where ...... 語句,只查詢一次。
現在往 SQL Server 中新建 mydb 數據庫,并創建 books 表。
CREATE TABLE [dbo].[books] ( [book_id] UNIQUEIDENTIFIER DEFAULT (newid()) NOT NULL, [title] NVARCHAR (35) NOT NULL, [desc] NVARCHAR (100) NULL, [author] NVARCHAR (20) NOT NULL, PRIMARY KEY CLUSTERED ([book_id] ASC) );
順便向表中插入些測試數據。
insert into books (title, author, [desc]) values (N'R語言從盜墓到考古', N'張法師', N'一套不正經的R語言退隱教程'), (N'瘋言瘋語之HTML 6.0', N'老周', N'提前一個世紀出現的超文本協議'), (N'程序員風水學', N'孫電鷹', N'先匪后將的他,曾有“東陵大盜”之稱,在盜掘過程中他學會了用風水理論去Debug項目'), (N'雞形機器人編程入門', N'老周', N'未來,由于長期不種植作物,人類只能躺在病床上依靠吮吸預制營養液維持生命;后有人提出開發雞形機器人,幫助人類進食')
現在,咱們試一下。
using var context = new MyDbContext(); foreach(Book bk in context.MyBooks) { Console.WriteLine($"{bk.Author,-10}{bk.Title}"); }
如果日志啟用,那么,你會看到,DbContext 從初始化到 foreach 循環訪問數據,只生成了一條 SQL 語句。
SELECT [b].[book_id], [b].[author], [b].[desc], [b].[title] FROM [books] AS [b] WHERE [b].[author] = N'老周'
下面來看看另一種應用情形——映射表值函數。
先在 SQL Server 中創建一個內聯表值函數,名為 get_all_books,返回表中所有行。
CREATE FUNCTION [dbo].[get_all_books]() RETURNS TABLE RETURN select * from dbo.books;
回到 .NET 項目,咱們要映射一下函數。
A、先在 DbContext 的派生類中定義一個方法,用于映射到函數,不需要實現方法體,直接拋異常就行。
internal IQueryable<Book> GetAllBooksMap() { throw new NotSupportedException(); }
實際上,EF Core 并不會真正調用方法,只是通過生成表達式樹 + 反射出方法名,然后再找到與方法名對應的數據庫中的函數罷了。所以,方法不需要實現代碼。
B、OnModelCreating 方法要改一下,映射列名的 HasColumnName 方法不能在 ToTable 方法中配置,否則表值函數返回的實體不能正確映射。
modelBuilder.Entity<Book>(t => { t.ToTable("books"); t.HasKey(t => t.BookId); t.Property(x => x.BookId).HasColumnName("book_id"); t.Property(z => z.Title).HasColumnName("title"); t.Property(k => k.Description).HasColumnName("desc"); t.Property(f => f.Author).HasColumnName("author"); });
也就是列名映射要在 Property 上配置,不能在 TableBuilder 上配置。
C、HasDbFunction 映射函數。
// 注意數據庫中的函數名與類方法不同 modelBuilder.HasDbFunction(GetType().GetMethod(nameof(GetAllBooksMap), BindingFlags.NonPublic)!).HasName("get_all_books");
這里有個誤區:很多大伙伴以為這樣就完事了,然后就開始調用代碼了。
using var context = new MyDbContext(); foreach(Book bk in context.GetAllBooksMap()) { Console.WriteLine($"{bk.Author,-10}{bk.Title}"); }
你以為這樣是對的,但運行后就是錯的。上面不是說了嗎?GetAllBooksMap 方法是沒有實現的,你不能直接調用它!不能調用,不能調用,不能調用!!
我們還需要再給 DbContext 的派生類再定義一個方法,使用 FromExpression 方法讓 GetAllBooksMap 轉為表達式樹。
public IQueryable<Book> GetAllBooks() { return this.FromExpression(() => GetAllBooksMap()); }
這么一來,GetAllBooksMap() 就成了表達式樹,EF 不會真的調用它,只是獲取相關信息,再翻譯成 SQL。
然后這樣用:
using var context = new MyDbContext(); foreach(Book bk in context.GetAllBooks()) { Console.WriteLine($"{bk.Author,-10}{bk.Title}"); }
看,四條記錄就讀出來了。

可是,你也發現了,這TM太麻煩了,為了表值函數映射,我要封裝兩個方法成員。其實,這里可以把兩個方法合成一個:
public IQueryable<Book> GetAllBooks() { return this.FromExpression(() => GetAllBooks()); }
由于是公共方法,OnModelCreating 中的 HasDbFunction 代碼也可以精簡一下。
modelBuilder.HasDbFunction(GetType().GetMethod(nameof(GetAllBooks))!).HasName("get_all_books");
這時候你又搞不懂了,What?GetAllBooks 方法怎么自己調用了自己?不不不,沒有的事,你又忘了,FromExpression 只是轉換為表達式樹,并不會真的調用它。所以,這樣合并后,其實代碼是這樣走的:
1、訪問 context.GetAllBooks() ,這時候,GetAllBooks 方法確實被調用了,是你的代碼調用的,不是EF調用;
2、GetAllBooks 方法被你調用后,FromExpression 方法被調用;
3、FromExpression 方法參數中,lambda 表達式雖然又引用了一次 GetAllBooks 方法,但這一次它不會被調用,EF Core 只是用來獲取方法名。
現在明白了嗎?
對,微軟官方文檔中的示例用的就是這種合并的方法,表面上看好像自己調用了自己,實則不會。
好了,今天就水到這里。

浙公網安備 33010602011771號