搜访稍即比 90% 的人细心的大伙伴一定发现了 DbContext 类有一个方法叫 FromExpression它到底干吗用的官方文档中没有专门的介绍只在表值函数映射的例子中看到。咱们先来看看此方法的签名IQueryable FromExpression(Expression expression)看着好像很复杂的样子。其实不咱们来拆解一下1、TResult 是类型参数泛型的知识点没忘吧这里其实指的就是实体类型比如你的爱狗 Dog。2、这个方法返回 IQueryable 类型说明允许你使用 LINQ 查询。3、重点理解其参数——Expression 表达有个万能规律可以把与 TDelegate 类型兼容的 lambda 表达式直接赋值给 Expression 变量。即这个 FromExpression 方法可以使用以下 lambda 表达式作为参数() [返回 IQueryable]这个委托的意思就是它单身狗无参数一枚但可以生产 IQueryable 对象。哦说了一大堆还没说这个方法到底有啥毛用。它的用处就是你可以指定一个表达式让 EF 一开始就返回筛选过的查询。在 DbContext 的派生类中声明 DbSet 类型的带 get set 的公共属性这种生成首个查询根查询是最常用的方案这个相信大伙们都很熟了EF Core 最基础操作老周就不多介绍了。假如你要对查询的初始数据做筛选那么按照 DbSet 的方案要先执行一下 SELECT **** FROM #$$#* 语句然后再执行 SELECT **** FROM *^$ WHERE xxxxx我还没操作数据呢就执行了两次 SELECT 语句了。所以说如果你一开始并不打算提取所有数据那么直接从一开始就执行 SELECT **** FROM xxxx WHERE yyyy 多好何必多浪费一条 SQL 语句还有一种使用场景你的数据不是从某个表 SELECT 出来的而是从一个表值函数返回的这种情况也要借助 FromExpression 方法。不知道老周以上说明你是否明白不明白没关系咱们实战一下你就懂了。咱们先定义的实体复制代码////// 妖书实体///public class Book{////// 标识 主键///public Guid BookId { get; set; }////// 书名///public string Title { get; set; } string.Empty;////// 简介///public string? Description { get; set; }////// 作者///public string Author { get; set; } string.Empty;}复制代码然后继承 DbContext 类常规操作。复制代码public class MyDbContext : DbContext{protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){optionsBuilder.UseSqlServer(Server.\\TEST;Databasemydb;Integrated SecurityTrue;Trust Server CertificateTrue);// 配置日志//.LogTo(log Debug.WriteLine(log));}protected override void OnModelCreating(ModelBuilder modelBuilder){modelBuilder.Entity(t {t.ToTable(books, tb {tb.Property(x x.BookId).HasColumnName(book_id);tb.Property(zz.Title).HasColumnName(title);tb.Property(k k.Description).HasColumnName(desc);tb.Property(f f.Author).HasColumnName(author);}).HasKey(t t.BookId);});}// 以下行现在不需要了//public DbSet Books { get; set; }public IQueryable MyBooks FromExpression(() Set().Where(x x.Author 老周));}复制代码这里不用再定义 DbSet 类型的属性了因为我们要对数据进行筛选重点看 MyBooks 属性的实现public IQueryable MyBooks FromExpression(() Set().Where(x x.Author 老周));Set() 方法的调用会让 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 (NR语言从盗墓到考古, 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 TABLERETURN select * from dbo.books;回到 .NET 项目咱们要映射一下函数。A、先在 DbContext 的派生类中定义一个方法用于映射到函数不需要实现方法体直接抛异常就行。internal IQueryable GetAllBooksMap(){throw new NotSupportedException();}实际上EF Core 并不会真正调用方法只是通过生成表达式树 反射出方法名然后再找到与方法名对应的数据库中的函数罢了。所以方法不需要实现代码。B、OnModelCreating 方法要改一下映射列名的 HasColumnName 方法不能在 ToTable 方法中配置否则表值函数返回的实体不能正确映射。复制代码modelBuilder.Entity(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 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});}看四条记录就读出来了。image可是你也发现了这TM太麻烦了为了表值函数映射我要封装两个方法成员。其实这里可以把两个方法合成一个public IQueryable GetAllBooks(){return this.FromExpression(() GetAllBooks());}由于是公共方法OnModelCreating 中的 HasDbFunction 代码也可以精简一下。modelBuilder.HasDbFunction(GetType().GetMethod(nameof(GetAllBooks))!).HasName(get_all_books);这时候你又搞不懂了WhatGetAllBooks 方法怎么自己调用了自己不不不没有的事你又忘了FromExpression 只是转换为表达式树并不会真的调用它。所以这样合并后其实代码是这样走的1、访问 context.GetAllBooks() 这时候GetAllBooks 方法确实被调用了是你的代码调用的不是EF调用2、GetAllBooks 方法被你调用后FromExpression 方法被调用3、FromExpression 方法参数中lambda 表达式虽然又引用了一次 GetAllBooks 方法但这一次它不会被调用EF Core 只是用来获取方法名。现在明白了吗对微软官方文档中的示例用的就是这种合并的方法表面上看好像自己调用了自己实则不会。好了今天就水到这里。