🧶03 Entity Framework Core笔记
00 分钟
2023-12-19
2023-12-19
type
status
date
slug
summary
tags
category
icon
password
  • 1、Entity Framework Core(EF Core)是微软官方的ORM框架。优点:功能强大、官方支持、生产效率高、力求屏蔽底层数据库差异;缺点:复杂、上手门槛高、不熟悉EFCore的话可能会进坑。
  • 2、Dapper。优点:简单,N分钟即可上手,行为可预期性强;缺点:生产效率低,需要处理底层数据库差异。
  • 3、EF Core是模型驱动(Model-Driven)的开发思想,Dapper是数据库驱动(DataBase-Driven)的开发思想的。没有优劣,只有比较。
  • 4、性能: Dapper等≠性能高;EF Core≠性能差。
  • 5、EF Core是官方推荐、推进的框架,尽量屏蔽底层数据库差异,.NET开发者必须熟悉,根据的项目情况再决定用哪个。
对于后台系统、信息系统等和数据库相关开发工作量大的系统,且团队比较稳定,用EF Core;对于互联网系统等数据库相关工作量不大的系统,或者团队不稳定,用Dapper。

环境搭建

Migration数据库迁移

面向对象的ORM开发中,数据库不是程序员手动创建的,而是由Migration工具生成的。关系数据库只是盛放模型数据的一个媒介而已,理想状态下,程序员不用关心数据库的操作。 根据对象的定义变化,自动更新数据库中的表以及表结构的操作,叫做Migration(迁移)。 迁移可以分为多步(项目进化),也可以回滚。
  • 为了使用生成数据库的工具,Nuget安装Microsoft.EntityFrameworkCore.Tools,否则执行Add-Migration等命令会如下报错
  • 1、再在“程序包管理器控制台”中执行如下命令 Add-Migration InitialCreate 会自动在项目的Migrations文件夹中中生成操作数据库的C#代码。讲解一下生成代码的作用。 InitialCreate是什么? 是对项目的声明
  • 2、代码需要执行后才会应用对数据库的操作。“程序包管理器控制台”中执行Update-database。
  • 3、查看一下数据库,表建好了

深入研究Migrations

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

Migrations其他命令

  • 1、Update-Database XXX 把数据库回滚到XXX的状态,迁移脚本不动。
  • 2、Remove-migration 删除最后一次的迁移脚本
  • 3、Script-Migration 生成迁移SQL代码。有了Update-Database 为什么还要生成SQL脚本。
3、Update-database只适合在开发环境下使用。很多公司是要求对生产环境数据库的操作要审计。
如果生产数据库已经是处于某个迁移版本的状态了,那么可以生成这个版本到某个新版本的SQL脚本。
4、开发环境用Update-database,生产环境根据公司需要选择是Update-database还是生成SQL脚本。
  • 可以生成版本D到版本F的SQL脚本: Script-Migration D F 生成版本D到最新版本的SQL脚本:Script-Migration D
  • 因此迁移脚本不能跨数据库。通过给Add-Migration命令添加“-OutputDir”参数的形式来在同一个项目中为不同的数据库生成不同的迁移脚本。本节简化为建不同项目

修改表结构

  • 1、项目开发中,根据需要,可能会在已有实体中修改、新增、删除表、列等。
  • 2、想要限制Title的最大长度为50,Title字段设置为“不可为空”,并且想增加一个不可为空且最大长度为20的AuthorName(作者名字)属性。 首先在Book实体类中增加一个AuthorName属性
  • 3、修改BookEntityConfig
    • 4、执行Add-Migration AddAuthorName_ModifyTitle。取名字有意义
    • 5、Update-Database

    增删改查

    插入数据

    • 1、只要操作Books属性,就可以向数据库中增加数据,但是通过C#代码修改Books中的数据只是修改了内存中的数据。对Books做修改后,需要调用DbContext的异步方法SaveChangesAsync()把修改保存到数据库。也有同步的保存方法SaveChanges(),但是用EF Core都推荐用异步方法。
    • 2、EF Core默认会跟踪(Track)实体类对象以及DbSet的改变。

    查询数据

    • 1、DbSet实现了IEnumerable<T>接口,因此可以对DbSet实施Linq操作来进行数据查询。EF Core会把Linq操作转换为SQL语句。面向对象,而不是面向数据库(SQL)。
    • 2、
      • 3、可以使用OrderBy操作进行数据的排序

        插入

        1、要对数据进行修改,首先需要把要修改的数据查询出来,然后再对查询出来的对象进行修改,然后再执行SaveChangesAsync()保存修改。

        删除

        批量修改、删除

        • 1、目前批量修改、删除多条数据的方法。 局限性:性能低:查出来,再一条条Update、Delete,而不能执行Update ... where;delete ... where;
        • 2、官方目前还没有支持高效的批量Update、Delete,有在后续版本中增加,但是目前只是前期意见征询阶段。
        • 3、杨中科实现了一个开源的高效批量修改、删除的开源项目。

          约定配置

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

          两种配置方式

          • 1、Data Annotation 把配置以特性(Annotation)的形式标注在实体类中。 [Table("T_Books")] public class Book { } 优点:简单;缺点:耦合。
          • 2、Fluent API builder.ToTable("T_Books"); 把配置写到单独的配置类中。 缺点:复杂;优点:解耦
          • 3、大部分功能重叠。可以混用,但是不建议混用。

          Fluent API

          • 1、视图与实体类映射:
            • 2、排除属性映射:
              • 3、配置列名:
                • 4、配置列数据类型:
                  • 5、配置主键 默认把名字为Id或者“实体类型+Id“的属性作为主键,可以用HasKey()来配置其他属性作为主键。
                    • 支持复合主键,但是不建议使用。
                  • 6、生成列的值
                    • 7、可以用HasDefaultValue()为属性设定默认值
                      • 8、索引
                        • 复合索引
                          唯一索引:IsUnique();聚集索引:IsClustered()
                          Fluent API中很多方法都有多个重载方法。比如HasIndex、Property()。 把Number属性定义为索引,下面两种方法都可以: builder.HasIndex("Number"); builder.HasIndex(b=>b.Number);
                          推荐使用HasIndex(b=>b.Number)、Property(b => b.Number)这样的写法,因为这样利用的是C#的强类型检查机制
                      • 9... 用EF Core太多高级特性的时候谨慎,尽量不要和业务逻辑混合在一起,以免“不能自拔”。比如Ignore、Shadow、Table Splitting等……

                      主键

                      • 1、 EF Core支持多种主键生成策略:自动增长;Guid;Hi/Lo算法等。
                      • 2、自动增长。
                        • 优点:简单;缺点:数据库迁移以及分布式系统中比较麻烦;并发性能差。
                          long、int等类型主键,默认是自增。因为是数据库生成的值,所以SaveChanges后会自动把主键的值更新到Id属性。试验一下。场景:插入帖子后,自动重定向帖子地址。
                      • 3、自增字段的代码中不能为Id赋值,必须保持默认值0,否则运行的时候就会报错。

                      Guid主键

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

                      其他方案

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

                      反向工程

                      • 1、根据数据库表来反向生成实体类
                      • 2、
                        • 1、生成的实体类可能不能满足项目的要求,可能需要手工修改或者增加配置。 2、再次运行反向工程工具,对文件所做的任何更改都将丢失。 3、不建议把反向工具当成了日常开发工具使用,不建议DBFirst。
                          “模型驱动”与“数据库驱动”
                          DBFirst:前期很爽,后期越来越痛苦。
                      EF Core把C#代码转换为SQL语句的框架。

                      通过代码查看EF Core生成的SQL

                      方法1:标准日志

                      方法2:简单日志

                      方法3:ToQueryString

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

                      使用MySql

                      EF Core一对多关系配置

                      • 1、所谓“关系数据库”
                      • 2、复习:数据库表之间的关系:一对一、一对多、多对多。
                      • 3、EF Core不仅支持单实体操作,更支持多实体的关系操作。
                      • 4、三部曲:实体类中关系属性;FluentAPI关系配置;使用关系操作。

                      一对多:实体类

                      • 1、文章实体类Article、评论实体类Comment。一篇文章对应多条评论。

                      一对多:关系配置

                      一对多:试验

                      • 1、迁移生成数据库表。
                      • 2、编写代码测试数据插入。
                      • 3、不需要显式为Comment对象的Article属性赋值(当前赋值也不会出错),也不需要显式地把新创建的Comment类型的对象添加到DbContext中。EF Core会“顺竿爬”。
                      优化:

                      一对多特殊类型自引用的组织结构树

                      EF Core一对一关系配置

                      • 在“一对多”的关系中,很显然是需要在“多”端有一个指向“一”端的列,因此除非我们需要显式的声明一个外键属性,否则EF Core会自动在“多”端的表中生成一个指向“一”端的外键列,不需要我们显式的声明一个外键属性。
                      • 对于一对一关系,由于双方是“平等”的关系,外键列可以建在任意一方,因此必须显式的在其中一个实体类中声明一个外键属性。

                      一对一:实体类

                      必须显式的在其中一个实体类中声明一个外键属性。

                      一对一:关系配置

                      一对一:试验

                      EF Core多对多关系配置

                      • 1、多对多:老师—学生。
                      • 2、EF Core 5.0开始,才正式支持多对多
                      • 3、需要中间表,举例数据。

                      多对多:实体类

                      多对多:关系配置

                      在表中要引入一张中间表

                      IQueryable的延迟执行

                      “IQueryable”的意思是“可查询的”,可以查询、但是没有执行查询,查询的执行被延迟了。
                      • 1、IQueryable只是代表一个“可以放到数据库服务器去执行的查询”,它没有立即执行,只是“可以被执行”而已。
                      • 2、对于IQueryable接口调用非终结方法的时候不会执行查询,而调用终结方法的时候则会立即执行查询。
                      • 3、终结方法:遍历、ToArray()、ToList()、Min()、Max()、Count()等;
                      • 4、非终结方法:GroupBy()、OrderBy()、Include()、Skip()、Take()等。
                      • 5、简单判断:一个方法的返回值类型如果是IQueryable类型,那么这个方法一般就是非终结方法,否则就是终结方法。
                      1、IQueryable代表一个对数据库中数据进行查询的一个逻辑,这个查询是一个延迟查询。我们可以调用非终结方法向IQueryable中添加查询逻辑,当执行终结方法的时候才真正生成SQL语句来执行查询。 2、可以实现以前要靠SQL拼接实现的动态查询逻辑。

                      EF Core分页查询

                      • 1、Skip(3).Take(8) 最好显式指定排序规则
                        • Skip() 跳过多少条
                        • Take() 取多少条
                      • 2、需要知道满足条件的数据的总条数:用IQueryable的复用
                        • LongCount和Count
                      • 3、页数:long pageCount = (long)Math.Ceiling(count * 1.0 / pageSize);

                      IQueryable底层是如何读取数据的

                      • 1、DataReader:分批从数据库服务器读取数据。内存占用小、 DB连接占用时间长;
                      • 2、DataTable:把所有数据都一次性从数据库服务器都加载到客户端内存中。内存占用大,节省DB连接。
                      IQueryable内部就是在调用DataReader
                      优点:节省客户端内存。 缺点:如果处理的慢,会长时间占用连接。
                      如何一次性加载数据到内存?
                      1、一次性加载数据到内存:用IQueryable的ToArray()、ToArrayAsync()、ToList()、ToListAsync()等方法。 2、等ToArray()执行完毕,再断服务器试一下。
                      =======================================================
                      1、场景1:遍历IQueryable并且进行数据处理的过程很耗时。 2、场景2:如果方法需要返回查询结果,并且在方法里销毁DbContext的话,是不能返回IQueryable的。必须一次性加载返回。 3、场景3:多个IQueryable的遍历嵌套。很多数据库的ADO.NET Core Provider是不支持多个DataReader同时执行的。把连接字符串中的MultipleActiveResultSets=true删掉,其他数据库不支持这个。
                      =======================================================

                      EF Core的异步方法

                      • 1、SaveChanges()、SaveChangesAsync()
                      • 2、异步方法大部分是定义在Microsoft.EntityFrameworkCore这个命名空间下EntityFrameworkQueryableExtensions等类中的扩展方法,记得using。
                      • 3、AddAsync()、AddRangeAsync()、AllAsync()、AnyAsync、AverageAsync、ContainsAsync、CountAsync、FirstAsync、FirstOrDefaultAsync、ForEachAsync、LongCountAsync、MaxAsync、MinAsync、SingleAsync、SingleOrDefaultAsync、SumAsync等
                      IQueryable的这些异步的扩展方法都是“立即执行”方法,而GroupBy、OrderBy、Join、Where等“非立即执行”方法则没有对应的异步方法。

                      EF Core执行的原生SQL语句

                      • 1、尽管EF Core已经非常强大,但是仍然存在着无法被写成标准EF Core调用方法的SQL语句,少数情况下仍然需要写原生SQL。
                      • 2、可能无法跨数据库。
                      • 3、三种情况:非查询语句、实体查询、任意SQL查询。
                      有SQL注入漏洞?
                      1、字符串内插的方式会不会有SQL注入攻击漏洞吗?查看一下执行的SQL语句吧。 2、字符串内插如果赋值给string变量,就是字符串拼接;字符串内插如果赋值给FormattableString变量,编译器就会构造FormattableString 对象。打印FormattableString的成员试试看。 3、ExecuteSqlInterpolatedAsync()的参数是FormattableString类型。因此ExecuteSqlInterpolatedAsync会进行参数化SQL的处理。

                      EFCore如何知道实体数据变了

                      快照更改跟踪:首次跟踪一个实体的时候,EF Core 会创建这个实体的快照。执行SaveChanges()等方法时,EF Core将会把存储的快照中的值与实体的当前值进行比较。
                      实体的状态
                      • 已添加(Added):DbContext正在跟踪此实体,但数据库中尚不存在该实体。
                      • 未改变(Unchanged):DbContext正在跟踪此实体,该实体存在于数据库中,其属性值和从数据库中读取到的值一致,未发生改变
                      • 已修改(Modified):DbContext正在跟踪此实体,并存在于数据库中,并且其部分或全部属性值已修改。
                      • 已删除(Deleted):DbContext正在跟踪此实体,并存在于数据库中,但在下次调用 SaveChanges 时要从数据库中删除对应数据。
                      • 已分离(Detached):DbContext未跟踪该实体。
                      “已分离”和“未改变”的实体,SaveChanges()忽略; “已添加”的实体,SaveChanges() 插入数据库; “已修改”的实体,SaveChanges() 更新到数据库; “已删除”的实体,SaveChanges() 从数据库删除;
                      设置不跟踪:
                      1、如果通过DbContext查询出来的对象只是用来展示,不会发生状态改变,则可以使用AsNoTracking()来 “禁用跟踪”。 2、分别加AsNoTracking()和不加,分别查看一个对象修改后的EntityEntry 信息。 3、如果查询出来的对象不会被修改、删除等,那么查询时可以AsNoTracking(),就能降低内存占用。

                      全局查询筛选器

                      悲观并发控制

                      实现:

                      乐观并发控制

                      乐观并发控制:RowVersion

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

                      EFCore-什么是表达式树?

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