Entity Framework Core

我想变成你,见你所见,爱你所爱。 --zhu
EF Core

ORM
定义:(Object Relational Mapping) 让开发者用对象操作的形式操作关系数据库。
ORM框架:EF Core ,Dapper, sqlSugar ,FreeSql等。

EF Core 与其他ORM比较
1、Entity Framework Core(EF Core)是微软官方的ORM框架。
优点:功能强大、官方支持、生产效率高、力求屏蔽底层数据库差异;
缺点:复杂、上手门槛高、不熟悉EFCore的话可能会进坑。
2、Dapper。
优点:简单,N分钟即可上手,行为可预期新强(原生sql语句);
缺点:生成效率低,手写SQL语句,需要处理底层数据库差异,不同数据库sql语句不同。
3、EF Core是模型驱动(Model-Driven)的开发思想,Dapper是数据库驱动(DataBase-Driven)的开发思想。各有优劣
4、性能:Dapper!=高,EF Core!=差。
5、EF Core是官方推荐、推进的框架,尽量屏蔽底层数据库差异, .NET开发者必须熟悉,根据的项目情况再决定用哪个。

如何选择
1、建议:对于后台系统、信息系统等和数据库相关工作量大的系统,且团队比较稳定,用EF Core;对于互联网系统等数据库相关工作量不大的系统,或者团队不稳定,用Dapper。
2、在项目中可以混用,只要注意EF Core的缓存、Tracking等问题。

EF Core与EF比较
1、EF有DB First、Model First、Code First。EF Core不支持模型优先,推荐使用代码优先,遗留系统可以使用Scaffold-DbContext来生成代码实现类似DBFirst的效果,但是推荐用Code First。
2、EF会对实体上的标注做校验,EF Core追求轻量化,不校验。
3、熟悉EF的话,掌握EF Core容易,很多用法移植而来。
4、EF中一些类的命名空间以及方法名称在EF Core稍有不同。
5、EF不再做新特性增加。

选择合适数据库
1、EF Core是对于底层ADO.NET Core的封装,因此ADO.NET Core支持的数据库不一定被EF Core支持。
2、EF Core支持所有主流的数据库,包括MS SQL Server,Oracle,MySQL,PostgreSQL,SQLite等。可以自己实现Provider支持其他数据库。
3、对于SQLServer支持最完美,MySQL,PostgreSQL也不错(有能解决的小坑)。这三者是.NET圈用的最多的三个。EF Core能尽量屏蔽底层数据库差异。

约定配置
主要规则:
1、表名采用DbContext中对应的DbSet的属性名。
2、数据表列的名字采用实体类属性的名字,列的数据类型采用和实体类属性类型的最兼容的类型。
3、数据表列的可空性取决与对应实体类属性的可空性。
4、名字为Id的属性为主键,如果主键为short,int或者long类型,则默认采用自增字段,如果主键为Guid类型,则默认采用默认的Guid生成机制生成主键值。

两种配置方式
1、Data Annotation
把配置以特性(Annotation)的形式标注在实体类中。优点:简单;缺点:耦合

[Table("T_Books")]
public class Book
{
  public long Id {get;set;}

  [Required]
  [MaxLength(50)]
  public string Name {get;set;}
}

2、FluentAPI
把配置写到单独的配置类中。缺点:复杂;优点:解耦。复杂的东西学会了后遇到坑概率更小。

builder.ToTable("T_Books");
builder.Property(b=b.Title).HasMaxLength(50).IsRequired();

3、大部分功能重叠。可以混用,但不建议混用。

FluentAPI
1、视图与实体类映射:modelBuilder.Entity().ToView("blogsView");
2、排除属性映射:modelBuilder.Entity().Ignore(b=>b.Name);
3、配置列名:modelBuilder.Entity().Property(b=>b.BlogId).HasColumnName("blog_id");
4、配置列数据类型:builder.Property(e.e=>Title).HasColumnType("varchar(200)")
5、配置主键:默认把名字为Id或者“实体类型+Id”的属性作为主键,可以用HasKey()类配置其他属性作为主键。
modelBuilder.Entity().HasKey(c=>c.Nmuber);支持复合主键,但不建议使用。
6、生成列的值
modelBuilder.Entity().Property(b=>b.Number).ValueGeneratedOnAdd();
7、为属性设置默认值
modelBuilder.Entity().Property(b=>b.Age).HasDefaultValue(6);
8、索引
modelBuilder.Entity().HasIndex(b=>b.Url);
复合索引
modelBuilder.Entity().HasIndex(p=>new{p.FirstName,p.LastName});
唯一索引:IsUnique();聚集索引:IsClustered()
9、用EF Core太多高级特性的时候谨慎,尽量不要和业务逻辑混合在一起。比如Ignore,Shadow,Table Splitting等。

自增主键
1、EF Core支持多种主键生成策略:自动增长;Guid;Hi/Lo算法等。
2、自动增长。优点:简单;缺点:数据库迁移以及分布式系统中比较麻烦;并发性能差。long int等类型主键,默认是自增。因为是数据库生成的值,所以SaveChanges后会自动把主键的值更新到Id属性,SaveChanges后,Books.Id可以获取到值。
3、自增字段的代码中不能为Id赋值,必须保持默认值0,否则允许的时候就会报错。

Guid
1、Guid算法(或UUID算法)生成一个全局唯一的Id。适合分布式系统,在进行多数据库数据合并的时候很简单。优点:简单,高并发,全局唯一;缺点:磁盘空间占用大。
2、Guid值不连续。使用Guid类型做主键时,不能把主键设置为聚集索引。因为聚集索引是按照顺序保存主键的,因此用Guid做主键性能差,比如MySQL的InnoDB引擎中主键是强制使用聚集索引的。有的数据库支持部分连续的Guid,比如SQLServer中的NewSequentiaId(),但也不能解决问题。在SQLServer等中,不要把Guid主键设置为聚集索引;在MySQL中,插入频繁的表不要用Guid做主键。
3、既可以让EF Core给赋值,也可以手动赋值(推荐)。

其他方案
1、混合自增和Guid(非复合主键)。用自增列做物理的主键,而用Guid列做逻辑上的主键。把自增列设置为表的主键,而在业务上查询数据时候把Guid当主键用。在和其他表关联以及和外部系统通讯的时候(比如前端显示数据的标识的时候)都是使用Guid列。不仅保证了性能,而且利用了Guid的优点,而且减轻了主键自增性导致主键值可以被预测带来的安全问题。(http:www.baidu.com?Id=1)
2、Hi/Lo算法:EF Core支持Hi/Lo算法来优化自增列。主键值由两个部分组成:高位()和低位(),高位由数据库生成,两个高位(1000,2000)之间间隔若干值,由程序在本地生成低位,低位的值在本地自增生成(1001,1002,1003)。不同进程或者集群中不同服务器获取Hi值不会重复,而本地进程计算的Lo则可以保证在本地高效率的生成主键值。但是HiLo算法不是EF Core标准,可能不支持部分数据库。

关于Migration
创建:Add-Migration Init
1、使用迁移脚本,可以对当前连接的数据库执行编号更高的迁移,这个操作叫做“向上迁移”(Up),也可以执行把数据库回退到旧的迁移,这个操作叫“向下迁移”(Down)。
2、除非有特殊需要,否则不要删除Migration文件夹下的代码。
3、进一步分析Migrations下的代码。分析Up、Down等方法。查看Migration编号。
4、查看数据库的_EFMigrationsHistory表:记录当前数据库曾经应用过的迁移脚本,按顺序排序。

Migrations其他命令
1、Update-Database XXX:把数据库回滚到XXX状态,迁移脚本不动。
2、Remove-migration:删除最后一次的迁移脚本。
3、Script-Migration:生成迁移T-SQL代码。(生成环境可能不允许C#代码直接执行,需要sql脚本执行)
4、Script-Migration D F:生成版本D到版本F的SQL脚本(版本D已经有部分脚本了,如果Script-Migration D F会报错D版本的部分已存在)
5、Script-Migration D:生成版本D到最新版本的SQL脚本。

反向工程
DB First, Model First, CodeFirst(EF Core推荐用CodeFirst);DB First则要用到反向工程。旧系统不可避免的存在旧表,所以需要反向工程。
1、根据数据库表来反向生成实体类。
2、控制台输入代码:(注意要安装EF Core对应的Nuget包)

Scaffold-DbContext 'Server=.;Database=demo1;Trusted_Connection=True;MultipleActiveResultSets=true'   Microsoft.EntityFrameworkCore.sqlserver

注意:
1、生成的实体类可能不能满足项目的要求,可能需要手工修改或者增加配置。
2、再次运行反向工程工具,对文件代码更改会丢失重新生成。
3、不建议把反向工具当成日常开发工具使用,不建议DBFirst。

EF Core操作数据库原理
把C#代码转换为SQL语句的框架,是基于ADO.NET Core的。
1、SQL Server Profiler查看SQLServer数据库当前执行的SQL语句。

做不到
存在合法的C#语言无法被翻译为SQL语句的情况。
比如lambda表达式里含有自定义方法。

代码查看EFCore 的SQL语句
1、简单日志:写测试代码时用。Logging起手
2、标准日志:需要正式记录SQL给审核人员或者排查故障,用标准日志。
自己写代码过滤不需要消息。optionsBuilder.LogTo(Console.WriteLine)
3、ToQueryString():开发阶段,从复杂的查询操作中立即看到SQL。
(1)上面两种方式无法直接得到一个操作的SQL语句,而且在操作很多的情况下,容易混乱。
(2)EF Core的Where方法返回的是IQueryable类型,DbSet也实现了IQueryable接口。IQueryable有扩展方法ToQueryString()获取SQL。
(3)不需要真的执行查询才能获得,但也只能获取查询操作的语句。

IQueryable
为什么有IEnumerable还要IQueryable
对于Where方法,普通集合和DbSet调用是不同的。普通集合的版本(IEnumerable)是在内存中过滤(客户端评估),而IQueryable版本则是把查询操作翻译成SQL语句(服务器端评估),服务器端一般性能更高,但客户端可以降低服务器负载。

IEnumerable<Book> books =ctx.Books;
books.Where(b=>b.Price>1.1)
IEnumerable<Book> books =ctx.Books;
books.Where(b=>b.Price>1.1)

关于IQueryable延迟执行
1、IQueryable只是代表一个“可以放到数据库服务器去执行的查询”,它没有立即执行,只是“可以执行”而已。
2、对于IQueryable接口调用非终结方法的时候不会执行查询,而调用终结方法的时候则会立即执行。
3、终结方法:遍历、ToArray()、ToList()、Min()、Max()、Count()等;
4、非终结方法:GroupBy()、OrderBy()、Include()、Skip()、Take()等。
5、简单判断:一个方法的返回值类型如果是IQueryable类型,那么这个方法一般就是非终结方法,否则就是终结方法。

为什么要延迟执行
原因:根据不同的条件动态拼接查询条件,实现了以前靠SQL拼接实现的动态查询逻辑。在实际执行前,分布构建IQueryable。

分页查询
pageIndex(页码),pageSize(每页条数)
1、Skip((pageIndex-1)pageSize).Take(pageSize)最好显示指定排序规则。
2、需要知道满足条件的数据的总条数:用IQueryable复用。
3、页数:long pageCount=(long)Math.Celling(Count
10/pageSize)

IQueryable底层读取数据方式
1、DataReader:分批从数据库服务器读取数据。内存占用小、DB连接占用时间长;
2、DataTable:把所有数据都一次性从数据库服务器加载到客户端内存中,内存占用大,节省DB连接。
IQueryable内部是调用的DataReader
验证:用insert into select多插入些数据,加上Delay/Sleep遍历IQueryable。在执行过程中,停止SQLServer服务器,会有报错,说明是长连接,分批获取数据。
优点:节省客户端内存。
缺点:如果处理慢,会长时间占用连接,导致连接池满。
需要一次性加载到内存时:
1、用IQueryable的ToArray()、ToArrayAsync()、ToList()、ToListAsync()等方法。
2、等ToArray()执行完毕后再断服务器(让using在最外层等)。
需要一次性加载到内存场景:
1、遍历IQueryable并且进行数据处理很耗时
2、如果方法需要返回查询结果,并且在方法里销毁DbContext的话,不能返回IQueryable,需一次性加载返回。
3、多个IQueryable嵌套遍历。很多数据库 ADO.NET Core Provider不支持多个DataReader同时执行,会报错。
连接字符串中MultippleActiveResultSets=true允许多个执行,但SQL Server才有。

异步方法
1、SaveChanges()、SaveChangesAsync()
2、异步方法大部分定义在Microsoft.EntityFrameworkCore这个命名空间下EntityFrameworkQueryableExtensions等类中的扩展方法。
3、AddAsync()、AddRangeAsync()、AllAsync()、AnyAsync、AverageAsync、ContainsAsync、CountAsync、FirstAsync、FirstOrDefaultAsync、ForEachAsync、LongCountAsync等。
有的方法没有异步方法:
IQueryable的这些异步扩展方法都是”立即执行“方法,而GroupBy、OrderBy、Join、Where等”非立即执行“方法则没有对应异步。为什么?”非立即执行“方法并没有实际执行SQL语句,并不是消耗IO的操作。(IO,输入输出)

使用原生SQL语句
1、尽管EF Core已经非常强大,但是仍然存在无法被写出标准EF Core调用方法的SQL语句,少数情况下仍然需要写原生SQL。
2、可能无法跨数据库。
3、三种情况:非查询语句、实体查询、任意SQL查询。
非查询语句:
使用dbCtx.Database.ExecuteSqlInterpolated(),ExecuteSqlInterpolatedAsync()方法来执行原生的非查询SQL语句。

ctx.Database.ExecuteSqlInterpolatedAsync(@$"insert into T_Books(Title,PubTime,Price,Author) select Title,PubTime,Price,{aname} from T_Books where Price > {price}")

SQL注入漏洞:
解决方法:参数化SQL
字符串内插如果赋值给string变量,就是字符串拼接(有漏洞);字符串内插如果赋值给FormmattableString变量,编译器就会构造FormattableString对象。
ExecuteSqlInterpolatedAsync()参数是FormmattableString类型。因此ExecuteSqlInterpolatedAsync会进行参数化SQL处理。
除了ExecuteSqlInterpolatedAsync(),ExecuteSqlInterpolated(),还有ExecuteSqlRaw(),ExecuteSqlRawAsync()也可以执行原生SQL语句,但需要开发人员自己处理查询参数,不推荐使用。

实体查询语句:
1、如果要执行原生SQL是一个查询语句,并且查询结果也能对应一个实体,可以调用对应实体的DbSet的FromSqlInterpolated()方法来执行,同样使用字符串内插传递参数。

IQueryable<Book> books=ctx.Books.FromSqlInterpolated(@$"select * from T_Books
                    where DatePart(year,PubTime)>{year}
                    order by newid()");

2、FromSqlInterpolated()方法返回值是IQueryable类型,因此可以在执行之前,对IQueryable进行逻辑处理。如加上foreach(Book b in books.Skip(3).Take(6))
3、把只能用原生SQL语句写的逻辑用FromSqlInterpolated()执行,然后把分页、分组、二次过滤、排序、Include等其他逻辑使用EF Core标准操作实现。
局限性:
SQL查询必须返回实体类型对应数据库表的所有列;结果集中的列名必须与属性映射到列名称匹配;只能单表查询,不能使用join语句进行关联查询。但是可以在查询后面使用include()来进行关联数据的获取。

任意SQL查询:
1、FromSqlInterpolated()只能单表查询,但是在实现报表查询的时候,SQL语句复杂,要多表join,返回结果不是一个实体对应。
2、EF Core允许把视图或存储过程映射为实体,所以可以把复杂查询语句写成视图或存储过程,然后声明对应实体,并且DbContext中配置对应DbSet。
3、不推荐存储过程;项目复杂查询多使视图太多;非实体的DbSet膨胀。

总结以上三类:
一般Linq操作就行,尽量不写原生SQL
1、非查询SQL用ExecuteSqlInterpolated()
2、针对实体的SQL查询用FromSqlInterpolated()
3、复杂SQL查询用ADO.NET的方式或者Dapper等。

如何批量删除、新增、修改
EF Core7开始官方才提供批量,之前版本没有,都是生成大串sql语句,而不是简单的update student set age ='17' where age ='18'效率不行,之前版本建议使用Zack.EFCore.Batch。
从EF Core7开始,可以使用 ExecuteUpdate 和 ExecuteDelete 方法更高效地执行相同的操作:

context.Employees.ExecuteUpdate(s => s.SetProperty(e => e.Salary, e => e.Salary + 1000));

全局查询筛选器
EF Core会自动将这个查询筛选器应用于涉及这个实体类型的所有LINQ查询。
场景:软删除、多租户。
软删除:不是真的delete ,而是加字段IsDeleted,软删除就是把它改成true,默认为false。
用法:builder.HasQueryFilter(b=>b.IsDeleted==false)
想要某个方法不用:ctx.Books.IgnoreQueryFilters().Where(b=>b.Title.Contains("o")).ToArray()

并发控制
避免多个用户同时操作资源造成并发冲突问题,例如添加点击量。
最好的解决方案:非数据库解决方案。
数据库层面两种策略:悲观、乐观。
悲观并发控制:一般采用行锁、表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源。EF Core没有封装悲观并发控制的使用,需要开发人员编写并发控制。不同数据库的语法不一样。尽量不用悲观
MySQL方案:select * from T_Houses where Id=1 for update,如果有其他查询操作也用for update来查询Id=1这条数据的话,那些查询被挂起,等这条数据更新完成释放锁,代码才会继续执行。

乐观并发控制

Update T_House set Owner=新值 where Id=1 and Owner=旧值

当Update的时候,如果数据库中owner值被修改,那么where语句值为false,受影响行数为0,EF Core知道发生并发冲突,SaveChange()方法抛出DbupdateConcurrencyException异常,用try-catch捕获。
配置:
1、把被并发修改的属性使用IsConcurrencyToken()设置为并发令牌。
2、builder.Property(h=>h.Owner).IsConcurrencyToken();
3、catch(DbupdateConcurrencyException ex){}

乐观并发
1SQLServer数据库可以用一个byte[]类型的属性做并发令牌属性,然后使用IsRowVersion()这个属性设置为RowVersion类型,数据库列则会被设置为RowVersion类型。RowVersion类型的列,在每次插入或者更新行时,数据库会自动为这一行RowVersion类型的列生成新值。(RowVersion=timestamp)

class House
{
public long Id {get;set;}
public byte[] RowVer {get;set;}
}
bulider.Property(h=>h.RowVer).IsRowVersion();

在MySQL等数据库虽然有类似timestamp类型,但是由于timestamp类型精度不够,不适合高并发系统。但是可以将并发令牌值更新为Guid的值:修改其他属性的同时,使用h1.RowVer=Guid.NewGuid()手动更新并发令牌属性的值。
总结:
1、乐观并发控制能够避免悲观锁带来的性能、死锁等问题,因此推荐使用乐观并发控制而不是悲观锁。
2、如果有一个确定的字段要被进行并发控制,那么使用IsConcurrencyToken()把这个字段设置为并发令牌即可。
3、如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且在每次更新数据的时候,手动更新这一列的值。如果用的是SQLServer数据库,那么也可以采用RowVersion列,这样不用开发者手动每次更新数据时,手动更新并发令牌的值。

表达式树
1、表达式树(Expression Tree):树形数据结构表示代码,以表示逻辑运算,以便可以在运行时访问逻辑运算的结构。
2、Expression类型
3、从lambda表达式来生成表达式树:
Expression<Func<Book,bool>> e1=b=>b.Price>5

Expression和委托的关系和区别:
Expression对象存储了运算逻辑,它把运算逻辑保存成抽象语法树(AST),可以在运行时动态获取运算逻辑。而普通委托则没有。
代码查看AST:
Install-Package ExpressionTreeToString

Expression<Func<Book,bool>> e1=b=>b.Price>5;
Console.WriteLine(e.ToString("Object notation","C#"));
posted on 2024-07-19 13:13  小脑虎爱学习  阅读(4)  评论(0编辑  收藏  举报