EF Core
EF Core
基本概念和原理
Entity Framework Core(EF Core)是微软官方的ORM框架
ORM:Object Relational Mapping,让开发者用对象操作的形式操作关系数据库
EF Core是模型驱动(Model-Driven)的开发思想
EF Core是官方推荐、推进的框架,尽量屏蔽底层数据库差异
优点:功能强大、官方支持、生产效率高、屏蔽底层数据库差异;
缺点:复杂、上手门槛高
EF Core 与 EF:
- EF有DB First、Model First、Code First。
- EF Core不支持模型优先,推荐使用代码优先,遗留系统可以使用Scaffold-DbContext来生成代码实现类似DBFirst的效果
- EF会对实体上的标注做校验,EF Core追求轻量化,不校验。
数据库支持:
- EF Core是对于底层ADO.NET Core的封装,因此ADO.NET Core支持的数据库不一定被EF Core支持。
- EF Core支持所有主流的数据库,包括MS SQL Server、Oracle、MySQL、PostgreSQL、SQLite等。
- 可以自己实现Provider支持其他数据库。
EFCore 底层原理
- 对数据库的访问仍然是通过 ADO.NET CORE
- EF Core 是把 C# 代码转换为 SQL 语句的框架
不同的数据需要提供对应的 Provider 负责翻译 SQL 语句
开发环境搭建
安装依赖包
# 核心包 Install-Package Microsoft.EntityFrameworkCore # Migration 依赖包 Install-Package Microsoft.EntityFrameworkCore.Tools # SqlServer 支持包 Install-Package Microsoft.EntityFrameworkCore.SqlServer
基本步骤
- 创建实体类
- 创建实体配置类 IEntityTypeConfiguration
- 约定大于配置,不进行配置将使用默认配置
- 推荐为每个实体创建单独的 IEntityTypeConfiguration 配置类,而不是在 DbContext 类中配置实体
- 创建 DbContext 类
- 配置 DbSet:对应数据库表
- 重写 OnConfiguring :配置连接字符串
- 重写 OnModelCreating:添加自定义实体配置
- 使用 Migration 生成数据库
- 使用 EF Core:
- 创建 DbContext 实例
- 根据上下文跟踪实体实例。
- 根据需要对所跟踪的实体进行更改以实现业务规则
- 调用 SaveChanges 或 SaveChangesAsync (如果有修改)
- 释放 DbContext 实例(可以使用 using 语法)
快速入门
创建实体类
internal class Book { public long Id { get; set; } public string Title { get; set; } public DateTime PubTime { get; set; } public double Price { get; set; } }
创建实体配置类 IEntityTypeConfiguration
internal class BookEntityConfig : IEntityTypeConfiguration<Book> { public void Configure(EntityTypeBuilder<Book> builder) { builder.ToTable("T_Books"); } }
internal class TestDbContext:DbContext { public DbSet<Book> Books { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); string connStr = "Server=localhost;Database=master;Trusted_Connection=True;"; optionsBuilder.UseSqlServer(connStr); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 从当前程序集获取所有实现了 IEntityTypeConfiguration 的配置类 modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); } }
Add-Migration InitialCreate Update-database
编写调用 EF Core 的业务代码
// DbContext 实现了 IDisposable 接口,需要手动释放资源 using(var ctx = new TestDbContext()){ foreach(var b in ctx.Books) Console.WriteLine(b.Title); }
实体配置
**(不推荐)**可以使用特性
优点:简单;缺点:耦合
把配置以特性(Annotation)的形式标注在实体类中
[Table("T_Books")] public class Book{}
**(推荐)**也可以使用 Fluent API
// 实体类与数据库表映射 builder.ToTable("T_Books"); // 修改 Title 列属性 builder.Property(e=>e.Title) .HasMaxLength(30) // 最大长度为 30 .IsRequired(); // 不可为空
实体属性配置
- 修改 IEntityTypeConfiguration 配置文件
- 用 EF Core太多高级特性的时候谨慎,尽量不要和业务逻辑混合在一起,以免“不能自拔”。比如Ignore、Shadow、Table Splitting……
// 排除属性映射
modelBuilder.Entity<Blog>().Ignore(b => b.Name2);
// 配置列名
modelBuilder.Entity<Blog>().Property(b =>b.BlogId).HasColumnName("blog_id");
// 配置列数据类型
builder.Property(e => e.Title) .HasColumnType("varchar(200)");
// 生成列的值
modelBuilder.Entity<Student>().Property(b => b.Number).ValueGeneratedOnAdd();
// 为属性设定默认值
modelBuilder.Entity<Student>().Property(b => b.Age).HasDefaultValue(6);
// 将私有成员映射成表字段
public record User
{
private string? passwordHash;
}
builder.Property("passwordHash");
// 将DDD值对象映射到当前实体对应的表中
public record User : IAggregateRoot
{
public PhoneNumber PhoneNumber { get; private set; }
}
builder.OwnsOne(x => x.PhoneNumber, nb => {
nb.Property(x => x.RegionCode).HasMaxLength(5).IsUnicode(false);
nb.Property(x => x.Number).HasMaxLength(20).IsUnicode(false);
});
约定配置规则
- 表名采用 DbContext 中的对应的 DbSet 的属性名
- 数据表列的名字采用实体类属性的名字
- 数据表列的数据类型采用和实体类属性类型最兼容的类型
- 数据表列的可空性取决于对应实体类属性的可空性
- 名字为Id的属性为主键,如果主键为short, int 或者 long类型,则默认采用自增字段,如果主键为Guid类型,则默认采用默认的Guid生成机制生成主键值
实体关系配置
关系术语
- **依赖实体:**这是包含外键属性的实体。 有时是指关系的“子级”。
- **主体实体:**这是包含主键/备选键属性的实体。 有时是指关系的“父级”。
- **主体键:**唯一标识主体实体的属性。 它可能是主键,也可能是备选键。
- **外键:**依赖实体中用于存储相关实体的主体键值的属性。
- **导航属性:**在引用相关实体的主体实体和/或依赖实体上定义的属性。
- **集合导航属性:**包含对许多相关实体的引用的导航属性。
- **引用导航属性:**保留对单个相关实体的引用的导航属性。
- **反向导航属性:**讨论特定导航属性时,此术语是指关系另一端上的导航属性。
- **自引用关系:**依赖实体类型与主体实体类型相同的关系。
约定配置
默认情况下,如果在类型上发现导航属性,将创建关系。
如果依赖实体包含一个名称与以下模式之一匹配的属性,则该属性将被配置为外键:
- <导航属性名><主键属性名>
- <导航属性名>Id
- <导航实体名><主键属性名>
- <导航实体名>Id
public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get; set; } } /* 数据库生成结果: 自动在多端添加外键指向一端,一端没有外键 T_Posts 表 PostId Title Content BlogId T_Blogs 表 BlogId Url */
手动配置导航
- 一对多:HasOne(…).WithMany(…);
- 一对一:HasOne(…).WithOne (…);
- 多对多:HasMany (…).WithMany(…);
- 不论是完全定义的关系还是单个导航属性,最终数据库都只会在多端添加外键指向一端,一般在一端配置
HasOne().HasMany()
- 如果在两个类型之间定义了多个导航属性(即不止一对指向彼此的导航), 需要对它们进行手动配置才能解决这种不明确的关系。
// 一对多配置演示
internal class BlogEntityConfig : IEntityTypeConfiguration<Blog>
{
public void Configure(EntityTypeBuilder<Blog> builder)
{
builder.ToTable("T_Blogs");
// 在关联的两个实体其中一个其实进行配置即可,EFCore 会自动推理设置另一个实体外键
builder.HasMany<Post>(e => e.Posts).WithOne(e => e.Blog);
}
}
/*
数据库生成结果: 自动在多端添加外键指向一端,一端没有外键
*/
获取数据
需要使用
.Include()
方法指定依赖的属性才能查询外键表,否则不会查找关联表,该属性将为空值# var post = ctx.Posts.Single(e => e.PostId == 1); SELECT TOP(2) [t].[PostId], [t].[BlogId], [t].[Content], [t].[Title] FROM [T_Posts] AS [t] WHERE [t].[PostId] = 1 # var post = ctx.Posts.Include(e => e.Blog).Single(e => e.PostId == 1); SELECT TOP(2) [t].[PostId], [t].[BlogId], [t].[Content], [t].[Title], [t0].[BlogId], [t0].[Url] FROM [T_Posts] AS [t] INNER JOIN [T_Blogs] AS [t0] ON [t].[BlogId] = [t0].[BlogId] WHERE [t].[PostId] = 1
测试
// 创建多端
var posts = new List<Post>() {
new Post(){ Title="Test Post Title 1",Content="Test Post Content 1" },
new Post(){ Title="Test Post Title 2",Content="Test Post Content 2" },
new Post(){ Title="Test Post Title 3",Content="Test Post Content 3" },
};
// 创建一端,将多端赋值到属性
var blog = new Blog() { Url="127.0.0.1",Posts= posts };
// 直接将一端添加到数据库, 多端的数据将自动添加到数据库
ctx.Blogs.Add(blog);
// 保存修改
await ctx.SaveChangesAsync();
// 查询数据
var blog = ctx.Blogs.Include(e => e.Posts ).Single(e=>e.BlogId==1);
foreach (var post in blog.Posts)
Console.WriteLine(post.Content);
var post = ctx.Posts.Include(e=>e.Blog).Single(e => e.PostId == 1);
Console.WriteLine(post.Blog.Url);
外键属性
- 实体类中直接使用实体类型的属性即可创建实体关联,数据库中会默认生成对应 <实体名>Id 外键属性列
- 从数据获取的实体却无法直接获得默认生成的 <实体名>Id 外键属性,必须关联查询获得对应实体,影响性能
- 因此,需要一种不需要Join直接获取外键列的值的方式
- 在实体类中显示声明外键属性:
public int BlogId {get;set;}
- 在实体配置类中将该属性声明为外键:
builder.HasOne<Blog>().WithMany(e=>e.Posts).HasForeignKey(e=>e.BlogTestId);
- 如果属性名符合约定,则不需要显示调用
.HasFreignkey()
方法 - 除非必要,否则不用声明,因为会引入重复。
- 在实体类中显示声明外键属性:
单项导航 or 双向导航
- 对于主从结构的“一对多”表关系,一般是声明双向导航属性。
- 而对于其他的“一对多”表关系:如果表属于被很多表引用的基础表,则用单项导航属性,否则可以自由决定是否用双向导航属性。
自引用的组织结构树
声明实体类:需要设置外键为可空,否则数据上传报外键约束冲突
internal class FileNode { public int Id { get; set; } public string Path { get; set; } public List<FileNode> Children { get; set; } = new List<FileNode>(); public FileNode? Parent { get; set; } // 需要设置外键为可空,否则数据上传报外键约束冲突 }
实体配置外键:
.OnDelete(DeleteBehavior.Restrict)
否则迁移表的时候会报错builder.HasOne<FileNode>(e => e.Parent).WithMany(e => e.Children).OnDelete(DeleteBehavior.Restrict);
查询数据
// 查询数据 var root = ctx.FileNodes.Include(e => e.Children).Single(e => e.Parent == null); PrintFileNode(ctx,root,0); // 递归打印节点路径 static void PrintFileNode(TestDbContext ctx,FileNode node,int indent) { Console.WriteLine(new string('-',indent)+node.Path); foreach(var child in node.Children) { var cur = ctx.FileNodes.Include(e => e.Children).Single(e => e.Id == child.Id); PrintFileNode(ctx, cur, indent+1); } }
一对一关系配置
- 对于一对一关系,由于双方是“平等”的关系,外键列可以建在任意一方,因此必须显式的在其中一个实体类中声明一个外键属性。
多对多关系配置
多对多的关系配置可以放到任何一方的配置类中,我这里把关系配置代码放到了Student类的配置中。
EFCore 会自动创建中间表,也可以自定义中间表
builder.HasMany<Teacher>(s => s.Teachers).WithMany(t=>t.Students).UsingEntity(j=>j.ToTable("T_Students_Teachers"));
主键
EF Core支持多种主键生成策略:自动增长;Guid;Hi/Lo算法等
// 配置主键,应采用约定配置的 Id 为主键,避免手动设置主键 // 支持复合主键,但是不建议使用 modelBuilder.Entity<Student>().HasKey(c => c.Number);
自动增长
long、int等类型主键,默认是自增。
优点:简单;
缺点:数据库迁移以及分布式系统中比较麻烦;并发性能差。
自增字段的代码中不能为Id赋值,必须保持默认值0,否则运行的时候就会报错。
Guid 主键
- 适合于分布式系统,在进行多数据库数据合并的时候很简单。
- 优点:简单,高并发,全局唯一;
- 缺点:磁盘空间占用大。
- Guid值不连续。使用Guid类型做主键的时候,不能把主键设置为聚集索引。
- 因为聚集索引是按照顺序保存主键的,每次插入Guid值时都会重新排序,影响性能。
混合自增和 Guid(非复合主键)
- 用自增列做物理的主键,而用Guid列做逻辑上的主键。
- 把自增列设置为表的主键,而在业务上查询数据时候把Guid当主键用。
- 在和其他表关联以及和外部系统通讯的时候(比如前端显示数据的标识的时候)都是使用Guid列。
索引
// 索引
modelBuilder.Entity<Blog>().HasIndex(b => b.Url);
// 复合索引
modelBuilder.Entity<Person>().HasIndex(p => new { p.FirstName, p.LastName });
// 唯一索引
modelBuilder.Entity<Blog>().HasIndex(b => b.Url).IsUnique();
// 聚集索引
IsClustered();
Migration 数据库迁移
- 面向对象的ORM开发中,数据库不是程序员手动创建的,而是由Migration工具生成的。
- 迁移可以分为多步(项目进化),也可以回滚。
- 常用命令
- Add-Migration : 添加迁移
- Update-database : 更新数据库,应用对数据库的操作
- Remove-migration :删除最后一次的迁移脚本
- Script-Migration D F:生成版本 D 到版本 F 迁移 SQL 代码,省略 F 则表示 D 到最新版本
- “向上迁移”(Up):使用迁移脚本对当前连接的数据库执行编号更高的迁移
- “向下迁移”(Down):使用迁移脚本把数据库回退到旧的迁移
- 除非有特殊需要,否则不要删除Migrations文件夹下的代码。
- 数据库的__EFMigrationsHistory表记录了当前数据库曾经应用过的迁移脚本,按顺序排列。
反向工程
- 根据数据库表来反向生成实体类
- 生成的实体类可能不能满足项目的要求,可能需要手工修改或者增加配置
- 再次运行反向工程工具,对文件所做的任何更改都将丢失
- 不建议把反向工具当成了日常开发工具使用,不建议 DBFirst
查看SQL语句
- 写测试性代码,用简单日志;
- 正式需要记录SQL给审核人员或者排查故障,用标准日志;
- 开发阶段,从繁杂的查询操作中立即看到SQL,用ToQueryString()
打印日志信息
internal class TestDbContext:DbContext
{
public static readonly ILoggerFactory MyLoggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); });
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// 标准日志
optionsBuilder.UseLoggerFactory(MyLoggerFactory);
// 简单日志
optionsBuilder.LogTo(Console.WriteLine,LogLevel.Information);
}
}
IQueryable 接口 ToQueryString 扩展方法
不需要真的执行查询才获取SQL语句;
只能获取查询操作的。
// 查询指定条件的数据 var res = ctx.Books.Where(e => e.Price > 50); Console.WriteLine(res.ToQueryString());
IQueryable 接口
- 普通集合的版本(IEnumerable)是在内存中过滤(客户端评估),而IQueryable版本则是把查询操作翻译成SQL语句(服务器端评估)
- 通常:客户端评估的性能比服务器端评估低
- IQueryable只是代表一个“可以放到数据库服务器去执行的查询”,它没有立即执行,只是“可以被执行”而已
- 对于IQueryable接口调用非终结方法的时候不会执行查询,而调用终结方法的时候则会立即执行查询
- IQueryable代表一个对数据库中数据进行查询的一个逻辑,这个查询是一个延迟查询
- IQueryable是一个待查询的逻辑,因此它是可以被重复使用的,可以实现分部构造查询条件
- ADO.NET 读取数据库的方式:
- DataReader : 分批从数据库服务器读取数据。内存占用小、 DB连接占用时间长;
- DataTable : 把所有数据都一次性从数据库服务器都加载到客户端内存中。内存占用大,节省DB连接。
- IQueryable 内部是在调用 DataReader 读取数据库,如果处理的慢,会长时间占用连接。
- 一次性加载数据到内存:用 IQueryable 的 ToArray()、ToArrayAsync()、ToList()、ToListAsync() 等方法
- 场景1:遍历 IQueryable 并且进行数据处理的过程很耗时。
- 场景2:如果方法需要返回查询结果,并且在方法里销毁DbContext的话,销毁 DBContext 后再执行 IQueryable 查询将报错。
- 场景3:多个 IQueryable 的遍历嵌套。很多数据库的ADO.NET Core Provider是不支持多个DataReader同时执行的。
- IQueryable 的异步扩展方法:
- 异步方法大部分是定义在 Microsoft.EntityFrameworkCore 这个命名空间
- AddAsync()、AddRangeAsync()、AllAsync()、AnyAsync、AverageAsync、ContainsAsync、CountAsync、FirstAsync、FirstOrDefaultAsync、ForEachAsync、LongCountAsync、MaxAsync、MinAsync、SingleAsync、SingleOrDefaultAsync、SumAsync
事务
using var trans = await ctx.Database.BeginTransactionAsync();
if (result.Succeeded) await trans.CommitAsync();
else trans.Rollback();
增删改查
查看转化后的SQL语句
在 DbContext 类中定义 Logger
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.LogTo(Console.WriteLine,LogLevel.Information); }
插入数据
- 对需要操作的 DbContext 属性(对应数据库表)进行增加数据操作(Add)
- 调用 DbContext 的异步方法 SaveChangesAsync 方法把修改保存到数据库
- EF Core默认会跟踪(Track)实体类对象以及DbSet的改变
查询数据
- DbSet 实现了 IEnumerable
<T
>接口,因此可以对 DbSet 实施 Linq 操作来进行数据查询。 - DbSet 的 Linq 操作实际定义在 Queryable 命名空间,返回值也是 IQueryable
<T
> 类型,IQueryable 继承了 IEnumerable - 终结方法:遍历、ToArray()、ToList()、Min()、Max()、Count() 等
修改、删除数据
- 要对数据进行修改,首先需要把要修改的数据查询出来,然后再对查询出来的对象进行修改,然后再执行 SaveChangesAsync() 保存修改
- 删除也是先把要修改的数据查询出来,然后再调用 DbSet 或者 DbContext 的 Remove 方法把对象删除,然后再执行 SaveChangesAsync() 保存修改
批量修改、删除数据
- 通过先查询再修改、删除数据的方式性能较低,因此 Entity Framework 7 增加了批量操作
- ExecuteDelete 和 ExecuteDeleteAsync
- ExecuteUpdate 和 ExecuteUpdateAsync
- 批量删除是级联操作
演示
// 插入数据
ctx.Books.Add(new Book() { Title="数学之美",Price=50,PubTime=DateTime.Now});
var c = await ctx.SaveChangesAsync();
Console.WriteLine($"插入 {c} 条数据");
// 查询所有数据
foreach (var b in ctx.Books)
Console.WriteLine(b.Title);
// 查询指定条件的数据
var res = ctx.Books.Where(e => e.Price > 50);
foreach(var b in res)
Console.WriteLine(b.Title);
// 修改数据
var res = ctx.Books.Where(e => e.Price > 50);
foreach (var b in res)
b.Price -=5;
await ctx.SaveChangesAsync();
// 删除数据
var res = ctx.Books.Where(e => e.Price > 100);
foreach (var b in res)
ctx.Remove(b);
await ctx.SaveChangesAsync();
// 批量修改
var n = await ctx.Books.Where(e => e.Title == "数学之美")
.ExecuteUpdateAsync(e =>
e.SetProperty(b => b.Price, b => b.Price + 5)
.SetProperty(b => b.PubTime, b => new DateTime(2001, 10, 3)));
await ctx.SaveChangesAsync();
Console.WriteLine($"更新 {n} 条数据");
// 批量删除
var n = await ctx.Books.Where(e=>e.Price<50)
.ExecuteDeleteAsync();
await ctx.SaveChangesAsync();
Console.WriteLine($"删除 {n} 条数据");
执行原生SQL语句
非查询语句
使用字符串内插方式:因为调用的是 ctx.Database 所以无需调用 ctx.Save
var title = "测试用书"; var price = 60; await ctx.Database.ExecuteSqlInterpolatedAsync(@$"insert into T_Books(Title,Price) select {title},Price from T_Books where Price < {price}");
字符串内插的方式不会有SQL注入攻击漏洞
字符串内插如果赋值给string变量,就是字符串拼接;字符串内插如果赋值给FormattableString变量,编译器就会构造FormattableString 对象。
ExecuteSqlInterpolatedAsync 会进行参数化SQL的处理。
// insert into T_Books(Title,Price) select @p0,Price from T_Books where Price < @p1
ExecuteSqlRaw()、ExecuteSqlRawAsync() 也可以执行原生SQL语句,但需要开发人员自己处理查询参数,否则可能会有SQL注入攻击漏洞
实体相关SQL语句
如果要执行的原生SQL查询的结果能对应一个实体,就可以调用对应实体的DbSet的FromSqlInterpolated()方法来执行查询SQL语句
var price = 50; var books = ctx.Books.FromSqlInterpolated<Book>($@"select * from T_Books where Price > {price}"); foreach(var book in books) Console.WriteLine(book);
FromSqlInterpolated() 方法的返回值是 IQueryable 类型,可以进一步筛选处理
局限性:
- SQL 查询必须返回实体类型对应数据库表的所有列;结果集中的列名必须与属性映射到的列名称匹配
- 如果只查询部分值,需要新建实体,并且为 DbContext 添加对应实体的 DbSet 属性,导致 DBContext 出现大量虚拟实体
- 只能单表查询,不能使用Join语句进行关联查询。但是可以在查询后面使用Include()来进行关联数据的获取。
使用 ADO.NET
使用 dbCxt.Database.GetDbConnection() 获得 ADO.NET Core 的数据库连接对象
// 获取连接对象并打开连接 using DbConnection conn=ctx.Database.GetDbConnection(); if(conn.State != System.Data.ConnectionState.Open) conn.Open(); // 创建 SQL 命令 using var cmd = conn.CreateCommand(); cmd.CommandText = @"select Title,Price from T_Books where Price > @price"; // 设置 SQL 参数 var param = cmd.CreateParameter(); param.ParameterName = "@price"; param.Value = 60; cmd.Parameters.Add(param); // 读取数据 using var reader = cmd.ExecuteReader(); while( reader.Read()) Console.WriteLine($"{reader.GetValue(0)}:{reader.GetValue(1)}");
【推荐】使用 Dapper
dapper是一款轻量级的ORM(Object Relationship Mapper),负责数据库和编程语言之间的映射
using var conn = ctx.Database.GetDbConnection(); // 使用动态类型 var items = conn.Query(@"select Id,Title from T_Books where Price > @price", new { price=80}); foreach(var row in items) Console.WriteLine(row); // {DapperRow, Id = '2', Title = '建筑设计深度图示'} // 使用特定类型 var bookTitles = conn.Query<BookTitle>(@"select Id,Title from T_Books where Price > @price", new { price = 80 }); foreach (var row in bookTitles) Console.WriteLine(row); // BookTitle { Id = 2, Title = 建筑设计深度图示 } // 声明 Dto 类型,不用添加到 DbSet,所以不会引起项目的 DbSet 膨胀 record BookTitle { public long Id { get; set; } public string Title { get; set; } }
总结
- 一般查询 Linq 操作就够了,尽量不用写原生SQL
- 非查询 SQL 用 ExecuteSqlInterpolated ()
- 针对实体的 SQL 查询用 FromSqlInterpolated()
- 复杂 SQL 查询用 ADO.NET 的方式或者 Dapper
EFCore 更改跟踪
实体实例在以下情况下会被跟踪:
从针对数据库执行的查询返回
通过
Add
、Attach
、Update
或类似方法显示附加到 DbContext检测为连接到现有跟踪实体的新实体
实体状态
Detached
实体未被 DbContext 跟踪。Added
实体是新实体,并且尚未插入到数据库中。 这意味着它们将在调用 SaveChanges 时插入。Unchanged
实体自从数据库中查询以来尚未进行更改。 从查询返回的所有实体最初都处于此状态。Modified
实体自从数据库中查询以来已进行更改。 这意味着它们将在调用 SaveChanges 时更新。Deleted
实体存在于数据库中,但标记为在调用 SaveChanges 时删除。
获取实体跟踪信息对象 EntityEntry
// 使用 EntityEntry.State 查看跟踪状态 var book1 = new Book(); ctx.Books.Add(book1); Console.WriteLine(ctx.Entry(book1).State); // Added var book2 = ctx.Books.First(); Console.WriteLine(ctx.Entry(book2).State); // Unchanged book2.Price = 100; Console.WriteLine(ctx.Entry(book2).State); // Modified var book3 = ctx.Books.First(); ctx.Remove(book3); Console.WriteLine(ctx.Entry(book2).State); // Deleted Console.WriteLine(ctx.Entry(new Book()).State); // Detached // 使用 EntityEntry.DebugView.LongView 查看详细信息 var book = ctx.Books.First(); book.Price= 10; Console.WriteLine(ctx.Entry(book).DebugView.LongView); /* Book {Id: 1} Modified Id: 1 PK Price: 10 Modified Originally 55 Title: '数学之美' */
性能优化:禁用跟踪,使用 AsNoTracking() 方法
// 如果查询出来的对象不会被修改、删除等,那么查询时可以AsNoTracking(),就能降低内存占用 var book = ctx.Books.AsNoTracking().First(); Console.WriteLine(ctx.Entry(book).State); // Detached
【不推荐】手动修改 EntityEntry
- 实现直接更新、删除数据,跳过查询数据的步骤
- 代码可读性、可维护性不强,而且使用不当有可能造成不容易发现的Bug。
// 删除数据 var entry1 = ctx.Entry(new Book() { Id =7 }); entry1.State = EntityState.Deleted; // 修改数据 var entry2 = ctx.Entry(new Book() { Id = 1, Price = 60.0 }); entry2.Property("Price").IsModified=true; // 执行操作 await ctx.SaveChangesAsync(); /* 直接操作数据,没有 Select 过程 SET NOCOUNT ON; DELETE FROM [T_Books] OUTPUT 1 WHERE [Id] = @p0; UPDATE [T_Books] SET [Price] = @p1 OUTPUT 1 WHERE [Id] = @p2; */
全局查询筛选器
在实体配置类中添加过滤器 HasQueryFilter
builder.HasQueryFilter(e => !e.IsDeleted);
foreach (var blog in ctx.Blogs) Console.WriteLine(blog.Url); /* 自动添加 IsDeleted 条件 SELECT [t].[BlogId], [t].[IsDeleted], [t].[Url] FROM [T_Blogs] AS [t] WHERE [t].[IsDeleted] = CAST(0 AS bit) */
忽略过滤器 IgnoreQueryFilters
foreach (var blog in ctx.Blogs.IgnoreQueryFilters()) Console.WriteLine(blog.Url);
全局筛选器的性能陷阱
- 由于自动添加查询条件,可能造成全表扫描
- 解决方法:可以将过滤条件和查询条件一起创建联合索引或聚集索引
并发控制
悲观并发控制
- 悲观并发控制一般采用行锁、表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源。
- EF Core没有封装悲观并发控制的使用,需要开发人员编写原生SQL语句来使用悲观并发控制。
- 示例:MySQL方案
- select * from T_Houses where Id=1 for update
- 如果有其他的查询操作也使用 for update 来查询 Id=1 的这条数据,那些查询就会被挂起,直到针对这条数据的更新操作完成释放行锁
- 悲观并发控制的使用比较简单
- 锁是独占、排他的,如果系统并发量很大的话,会严重影响性能,如果使用不当的话,甚至会导致死锁
【推荐】乐观并发控制
EFCore 采用乐观并发控制,在更新数据是会带上并发列的原始值进行校验
Update T_Houses set Owner=新值 where Id=1 and Owner=旧值
发生并发冲突时 SaveChanges() 方法会抛出 DbUpdateConcurrencyException 异常
把被并发修改的属性使用 IsConcurrencyToken() 设置为并发令牌
builder.Property(h => h.Owner).IsConcurrencyToken();
多个并发控制列需要引入额外列作为并发令牌列 rowversion
builder.Property(h => h.RowVer).IsRowVersion();
- SQLServer数据库可以用一个byte[]类型的属性做并发令牌属性,数据库列就会被设置为ROWVERSION类型,每次 Update 都会自动更新这一列
- 非 SQLServe r中,可以将并发令牌列的值设置为 Guid 的值,每次修改其他属性时手动更新令牌
h1.RowVer = Guid.NewGuid()
表达式树
表达式树(Expression Tree):树形数据结构表示代码,以表示逻辑运算,以便可以在运行时访问逻辑运算的结构。
Expression<TDelegate>
类型可以从 Lambda 表达式生成表达式树:
Expression<Func<Book, bool>> e1 = b =>b.Price > 5;
表达式树和普通委托区别:
Expression对象储存了运算逻辑,它把运算逻辑保存成抽象语法树(AST),可以在运行时动态获取运算逻辑。而普通委托则没有。
Expression<Func<Book, bool>> e = b => b.Title.Contains("数学") && b.Price < 100; /* 表达式树可以直接传递给 Where 翻译成对应的 SQL 语句 SELECT [t].[Id], [t].[Price], [t].[Title] FROM [T_Books] AS [t] WHERE ([t].[Title] LIKE N'%数学%') AND [t].[Price] < 100.0E0 */ Func<Book, bool> e = b => b.Title.Contains("数学") && b.Price<100; /* 委托传递给 Where 时不能解析为 SQL 语句,只能读取所有数据到内存,然后在调用委托过滤数据 SELECT [t].[Id], [t].[Price], [t].[Title] FROM [T_Books] AS [t] */ foreach (var book in ctx.Books.Where(e)) Console.WriteLine(book);
查看表达式树
安装依赖包:
Install-Package ExpressionTreeToString
Expression<Func<Book, bool>> e = b => b.Price < 100; Console.WriteLine(e.ToString("Object notation","C#"));
动态构造表达式树
调用 Expression 类的静态方法(创建表达式树的工厂方法)来生成各种表达式
- Add:加法;
- AndAlso:短路与运算;
- ArrayAccess:数组元素访问;
- Call:方法访问;
- Condition:三元条件运算符;
- Constant:常量表达式;
- Convert:类型转换;
- GreaterThan:大于运算符;
- GreaterThanOrEqual:大于或等于运算符;
- MakeBinary:创建二元运算;
- NotEqual:不等于运算;
- OrElse:短路或运算;
- Parameter:表达式的参数;
表达式举例
- ParameterExpression:参数表达式,比如 Book 类型的对象 book
- MemberExpression:成员表达式,比如点操作符 .
- MethodCallExpression:方法表达式,小括号 ()
- BinaryExpression:二元表达式,比如 <
- ConstantExpression :常量表达式
快速创建表达式树
创建简单的、近似的表达式树
Expression<Func<Book, bool>> e = b => b.Title.Contains("数学") && b.Price < 100;
查看表达式树代码
Console.WriteLine(e.ToString("Factory methods", "C#"));
修改代码形成需要的表达式树
// 静态 using 引入静态类 Expression 便于直接调用其方法 using static System.Linq.Expressions.Expression;
示例:根据动态输入条件查询
static IEnumerable<T> MyQuary<T>(string cond, object val) where T : class
{
using var ctx = new TestDbContext();
// 构建对象参数
var param = Parameter(typeof(T), "param");
// 获取对象属性值作为左表达式
var left = MakeMemberAccess(param, typeof(T).GetProperty(cond));
// 右表达式
Expression right = Constant(val);
// 进行相等判断
Expression<Func<T, bool>> expr = Lambda<Func<T, bool>>(MakeBinary(ExpressionType.Equal,left,right ),param);
// 将表达式树作为查询条件查询并返回列表
return ctx.Set<T>().Where(expr).ToList();
}
// 测试
foreach (var book in MyQuary<Book>("Title", "三体"))
Console.WriteLine(book);
/* 测试结果
SELECT [t].[Id], [t].[Price], [t].[Title]
FROM [T_Books] AS [t]
WHERE [t].[Title] = N'三体'
Book { Id = 4, Title = 三体, PubTime = , Price = 75 }
*/