ASP.NET Core学习笔记-2:EF Core 进阶知识
本文最后更新于:2024年11月2日 下午
EF Core 高级操作
IEnumerable 与 IQueryable
可以直接把IQueryable
当成 EF Core 特供版IEnumerable
(前者继承后者),它们的区别是:Enumerable
类中定义的供普通集合用的Where
等方法都是“客户端评估”,而Queryable
类中定义的供DbSet
等类用的Where
等方法是“服务器端评估”。
那么,“客户端评估”与“服务器端评估”又有什么不同呢?使用 SQL 语句在数据库服务器上完成数据筛选的过程叫作“服务器端评估”;把数据首先加载到应用程序的内存中,然后在内存中进行数据筛选的过程叫作“客户端评估”。因此对大部分情况而言,“客户端评估”的性能是比较低的,所以应该避免。IQueryable
跟LINQ
一样,对于非立即方法,都是延迟执行的。
“判断一个方法是否是立即执行的简单方式是:一个方法的返回值类型如果是
IQueryable
类型,这个方法一般就是非立即执行方法,否则这个方法就是立即执行方法。”
EF Core 分页展示
其实只要接触过LINQ
,如何分页展示这个问题都能即答吧(确信:那就是使用Skip(n)
与Take(m)
方法。以下是一个示例:
1 |
|
“在使用分页查询的时候有一个问题需要注意,那就是尽量显式地指定排序规则,因为如果不指定排序规则,那么数据库的查询计划对于数据的排序可能是不确定的。”
IQueryable 的底层运行
IQueryable
是用类似DataReader
的方式读取查询结果的(其实其内部的遍历就是在调用DataReader
进行数据读取),所以在遍历IQueryable
对象的过程,将数据库连接断开的话,程序就会报错。如果需要像DataTable
那样,一次性将所有数据读入内存中,可以使用ToArray()
、ToList()
等方法。
那么,什么时候需要将数据都读取到内存中呢?一是:方法需要返回查询结果的时候,因为IQueryable
是依赖上下文的,如果上下文被Disposed
了,依赖于此上下文的IQueryable
也会随之失效。二是:多个IQueryable
的遍历嵌套的时候,因为数据库可能不支持多个DataReader
同时执行,所以需要考虑将一部分查询结果直接读入到内存中。
SQL Server 可以通过在连接字符串中设置MultipleActiveResultSets=true
来开启“允许多个DataReader
执行”。
如何执行原生 SQL 语句
如果需要执行 SQL 非查询语句,可以使用dbCtx.Database.ExecuteSqlInterpolated
方法或其异步版本:await ctx.Database.ExecuteSqlInterpolated($"insert into T_Books (Title) Value ('{title}')")
。
如果需要执行 SQL 查询语句,并且查询的结果也能对应一个实体类,可以使用对应实体类的FromSqlInterpolated
方法:ctx.Books.FromSqlInterpolated($"select * from T_Books where Title = '{title}'")
。
如果需要执行任意 SQL 查询语句,可以通过dbCtx.Database.GetDbConnection
获得一个数据库连接,然后就可以直接调用 ADO.NET 的相关方法执行任意的 SQL 语句了。同时可以使用 Dapper 等轻量级的 ORM 工具来简化对 ADO.NET 的调用。
怎么知道实体类变化了
EF Core 默认采用:“快照跟踪改变”实现实体类改变的检测。在上下文首次跟踪一个实体类的时候,EF Core 会创建这个实体类的快照,当执行SaveChanges
等方法时,EF Core 将会把存储的快照中的值与实体类的当前值进行比较,已确定哪些属性值被更改了。实体类有如下 5 种可能的状态:
- 已添加(Added)
- 未改变(Unchanged)
- 已修改(Modified)
- 已删除(Deleted)
- 分离(Detached)
他们的具体说明请参考书本 P130。
使用上下文的Entry
方法,向其传入一个实体类对象,就可以获得该对象在 EF Core 中的跟踪信息对象EntityEntry
。EntityEntry
类的State
属性存放实体类的状态,DebugView.LongView
属性存放实体类的状态变化信息。
EF Core 性能优化
AsNoTracking
如果我们能够确认通过上下文查询出来的对象只是用来展示,不会发生状态改变(Added, Modified, Deleted),就可以在查询的时候使用AsNoTracking
来禁用跟踪:Book[] books = ctx.Books.AsNoTracking().Take(3).ToArray();
实体类状态跟踪的妙用
我们在对一个实体类进行修改时,一般都是先查后改:
1 |
|
上面的代码会执行两个SQL,一个Select
,一个Update
。
那么我们是否可以不执行Select
语句,直接进行Update
呢?通过利用状态跟踪机制就可以了:
1 |
|
上面的代码中,对Id = 10
的数据执行了一条Update
语句,对Id = 20
的数据执行了一条Delete
语句。
“…不过以这种方式编写的代码可读性、可维护性都不强,而且使用不恰当有可能造成不容易发现的 bug。大部分情况下,采用这种技巧带来的性能提升也是微乎其微的…”,省流:大部分情况下不建议用。
Find 和 FindAsync 方法
这两个方法会先在上下文查找这个对象是否已经被跟踪,如果对象已经被跟踪,就直接返回被跟踪的对象,只有在本地没有找到这个对象时,才会执行数据库查询,而Single
方法则是只执行数据库查询。(但是要注意的是Find
方法拿到的可能并不是最新的数据)
EF Core 中高效地删除、更新数据
很遗憾,EF Core 暂不支持批量删除或更新数据(每个主键都执行一条SQL),只能通过第三方库来实现。能用的第三方库有:Zack.EFCore.Batch
(杨中科老师自己写的库)、EFCore.BulkExtensions
(流行的第三方库),用法可以参考它们各自的文档。
全局查询筛选器
常见应用场景:“软删除”,“多租户”。
“软删除”:系统中对数据执行删除操作,并不会真正从数据库中删除对应的数据,而是将这些数据标记为“已删除”(比如增加一列IsDeleted
列,并把它的值设置为1
)。然后在查询的时候,会加上Where IsDeleted != 1
的条件,就达到了过滤被“删除”数据的目的。
但是每次执行查询都要自己手动加入这个条件会很麻烦,不过可以通过为实体类添加全局查询筛选器,让 EF Core 自动帮我们在查询时加上这个条件:
1 |
|
如果一些查询需要忽略掉筛选器的话,可以在查询时使用IgnoryQueryFilters()
:ctx.Books.IgnoryQureyFilters().Where(b => b.Id > 10);
悲观并发控制
采用行锁、表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源。
但是不同类型的数据库对于悲观并发控制的实现差异很大,所以 EF Core 没有封装悲观并发控制,需要自行编写原生 SQL 语句。
乐观并发控制
允许多个使用者同时操作同一个资源,通过冲突的检测避免并发操作。
EF Core 内置了使用并发令牌实现的乐观并发控制,并发令牌列通常就是被并发操作影响的列。例如,在执行Update
时,将某一列的旧值作为Where
条件:Update T_Books Set Title = 'New Title', DataStamp = DataStamp + 1 Where DataStamp = '2'
,不难看出,在这个 SQL 中,DataStamp 列就是并发令牌列。
在 EF Core 中,要使用乐观并发控制,对并发修改的属性使用IsConcurrencyToken()
即可将其设置成并发令牌列。如果上下文执行保存更改的时候抛出了DbUpdateConcurrencyException
异常,则说明更新的时候出现了并发修改冲突
表达式树
“表达式树(Expression tree)是用树形数据结构来表示代码逻辑运算的技术,它让我们可以在运行时访问逻辑运算的结构。”
表达式树在 .NET 中对应Expression<TDelegate>
类型,例如:Expression<Func<Book, bool>> e1 = b => b.Price > 5;
。
Expression 和 Func 的区别
Expression
是“服务器端评估”,Func
是“客户端评估”。Expression
对象存储了运算逻辑(使用Console.Write()
将对象输出到控制台,就知道这里想表达什么了),它把运算逻辑保存成 AST(abstract syntax tree,抽象语法树),我们可以在运行时动态分析运算逻辑。
查看表达式树
可以使用可视化工具来查看表达式树,例如用开源调试查看器:Expression Tree Visualizer(目前仅在 VS2017/VS2019 上测试过)
也可以使用代码来查看表达式树,需要安装 NuGet 包 ExpressionTreeToString,然后编写代码即可:Console.WriteLine(e.ToString("Object notation", "C#"));
;Console.WriteLine(e.ToString("Factory Methods", "C#"));
表达式树的节点类型
ConstantExpression
: 常量节点类型。BinaryExpression
: 二元运算符节点类型。MemberExpression
: 类成员访问操作节点类型。MethodCallExpression
: 方法调用操作节点类型。
通过代码动态构建表达式树
通过Expression
类的Parameter
、MakeBinary
、Call
、Constant
等静态方法(构建表达式树的工厂方法),我们可以动态构建表达式树。
举个例子,将Expression<Func<Book, bool>> e1 = b => b.Price > 5;
用工厂方法翻译一下就是:
1 |
|
当然,上面的代码还能更简单:
1 |
|
了解了工厂方法的基本用法后,就可以着手构建动态表达式树了!下面给出一个根据传入的属性名与值来查找数据的方法:
1 |
|
在运行时动态设定 Select 查询出来的属性
此部分内容请参考书本 p152 页。(个人认为还蛮有用的)
何时使用表达式树?
“一般只有在编写不特定于某个实体类的通用框架代码的时候,由于无法在编译期确定要操作的类名、属性等,才需要编写动态构建表达式树的代码,否则为了提高代码的可读性和可维护性,我们要尽量避免动态构建表达式树。”
一种好的方式是:用分布构建IQueryable
的方式来代替动态构建表达式树。
1 |
|
希望本文章能够帮到您~