6 EF Core
数据库迁移指令
安装dotnet ef
dotnet tool install --global dotnet-ef
生成迁移代码
dotnet ef migrations add "迁移名称"
更新到数据库中
dotnet ef database update ["迁移名称(可选, 回滚到指定迁移处)"]
删除最后一次迁移脚本
dotnet ef migrations remove
生成SQL代码
dotnet ef migrations script "迁移名称(起始, 默认是第一次迁移)" "迁移名称(结束, 默认是最后一次迁移)"
批量修改/删除
Nuget 包: Zack.EFCore.Batch(.NET 7 之前, .NET 7 之后已支持批量修改/删除)
Fluent API
- 视图与实体类映射(不推荐使用)
builder.ToView("blogsView")
- 表与实体类映射
builder.ToTable("T_Books")
- 排除属性映射
builder.Ignore(b => b.Name2)
- 配置列名
builder.Property(b => b.BlogId).HasColumnName("blog_id")
- 配置列数据类型
builder.Property(b => b.BlogId).HasColumnType("varchar(200)")
- 配置最大长度
builder.Property(b => b.Name).HasMaxLength(20)
- 配置不为空
builder.Property(b => b.BlogId).IsRequired()
- 配置默认值
builder.Property(b => b.Name).HasDefaultValue("Def")
- 配置主键
- 默认把名字为 Id 或者"实体类型+Id"的属性作为主键, 可以用
HasKey()来配置其它属性作为主键 builder.HasKey(b => b.Number)- 支持复合主键, 但不建议使用
- 默认把名字为 Id 或者"实体类型+Id"的属性作为主键, 可以用
- 配置索引
builder.HasIndex(b => b.Url)- 支持复合索引, 如
builder.HasIndex(b => new { b.FirstName, p.LastName } ) - 默认情况下定义的索引不是唯一索引, 使用
IsUnique()把索引设置为唯一索引 - 还可以使用
IsClustered()把索引设置为聚集索引
通过日志输出SQL语句的三种方式
// 使用简单日志
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//...
optionsBuilder.UseLoggerFactory(_loggerFactory);
//...
}
// 使用日志框架
using Microsoft.Extensions.Logging;
private static ILoggerFactory _loggerFactory = LoggerFactory.Create(p => p.AddConsole());
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//...
optionsBuilder.UseLoggerFactory(_loggerFactory);
//...
}
// 使用 ToQueryString
var query = ctx.Persons.AsQueryable();
// ...进行筛选
...
// 获取SQL语句
query.ToQueryString();
表的三种关系
一对多
在从表配置时使用(推荐使用): .HasOne(...).WithMany(...)
或在主表配置是使用: .HasMany(...).WithOne(...)
方法都传入参数后, 属于双向导航属性
如果只想获得外键ID而不要使用 Join 来进行额外一次的查询, 使用 HasForeignKey(p => p.外键ID属性) 来指定外键ID字段
使用单向导航属性(WithMany 不传参数即可): HasOne(p => p.xxx).WithMany();
双向导航属性: 如文章表和评论表的主从表关系
单向导航属性: 如用户表(基础表)和用户请假表的关系
特殊的一对多(自引用, 组织架构树)
public class OrgUnit
{
public long Id { get; set; }
public string Name { get; set; }
public OrgUnit Parent { get; set; }
public List<OrgUnit> Children { get; set; } = new();
}
public class OrgUnitConfig : IEntityTypeConfiguration<OrgUnit>
{
public void Configure(EntityTypeBuilder<OrgUnit> builder)
{
builder.ToTable("OrgUnit");
builder.Property(p => p.Name).HasMaxLength(50).IsUnicode().IsRequired();
// 根节点没有 Parent, 所以不能加 IsRequired()
// 表示说, 有一个 父节点, 这个父节点拥有许多个 子节点
builder.HasOne<OrgUnit>(p => p.Parent).WithMany(p => p.Children);
}
}
一对一
.HasOne(...).WithOne(...).HasForeignKey<...>(...)
配置到哪个实体类中都没问题, 同时必须声明外键属性(其中一个实体类存在外键属性字段即可)
多对多
.HasMany(...).WithMany(...).UsingEntity(p => p.ToTable("xxx"))
配置到哪个实体类中都没问题, 同时需要指定中间关系表的表名
客户端评估和服务器评估
IQueryable<Order> query = ctx.Orders;
// (服务器评估)IQueryable 会将 Where 筛选数据转换成SQL语句(在数据库中进行操作)
IQueryable<Order> order1 = query.Where(p => p.Name == "HELLO");
// (客户端评估)IEnumerable 会导致数据全部查询出来, 然后才进行 Where 筛选(在内存中进行操作)
IEnumerable<Order> order2 = query.Where(p => p.Name == "HELLO");
// 使用order1... 使用...order2
应尽量避免使用客户端评估(但有些情况下使用客户端评估效率会更好)
异步遍历IQueryable
- 使用 ToListAsync(), ToArrayAsync()
- 推荐在结果集不大的情况下使用
- 使用异步 foreach
await foreach(var item in ctx.Books.AsAsyncEnumerable())
- 一般也不需要这样做异步遍历, 普通遍历即可(除非遇到性能瓶颈, 可以使用异步遍历来优化)
执行SQL语句
执行非查询SQL语句
int rowAffected = await ctx.Database.ExecuteSqlInterpolatedAsync($"SELECT * FROM Users WHERE Id = {id}");
ExecuteSqlInterpolatedAsync() 方法参数是 FormattableString 类型, 因此传参时会进行参数化处理, 不会造成 SQL 注入攻击
还有 ExecuteSqlRaw() 可以执行 SQL 语句, 但需要自己处理查询参数, 不推荐使用
执行和实体相关的SQL语句
var titlePattern = "%中%";
IQueryable<Order> orders = ctx.Orders.FromSqlInterpolated(@$"SELECT * FROM Order WHERE title LIKE {titlePattern}");
方法返回的是一个 IQueryable<T> 类型, 不会立即执行 SQL 语句, 因此没有异步方法
把只能用原生 SQL 语句写的逻辑用这个方法去执行, 对于之后的分页/分组/二次过滤/Include() 等其它逻辑尽可能使用 EF Core 标准操作去实现
同样地还有 FromSqlRaw() 方法, 也不推荐使用
局限性
- SQL 查询必须返回实体类型对应数据库表的所有列
- 结果集中的列名必须与属性映射到的列名相匹配
- 只能单表查询, 不能使用 Join 语句进行关联查询, 但可以在查询后面使用 Include() 来进行关联数据的获取
执行任意SQL语句(使用ADO.NET)
using (DbCommand cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT * FROM Order GROUP BY Id";
using (var reader = await cmd.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
long id = reader.GetInt64(0);
string title = reader.GetString(1);
Console.WriteLine($"{id} - {title}");
}
}
}
不推荐使用, 如果实在需要执行这类语句, 推荐使用 Dapper 执行原生复杂 SQL
跟踪实体的状态
- 已添加(Added)
- 未改变(Unchanged)
- 已修改(Modified)
- 已删除(Deleted)
- 分离(Detached)
var newOrder = new Order();
// 通过上下文的 Entry() 方法, 可以获取到状态, 同时还可以进行状态修改
EntityEntry entry = ctx.Entry(newOrder);
Console.WriteLine(entry.State);
// 输出实体快照信息和修改情况
Console.WriteLine(entry.DebugView.LongView);
如果查询出来的数据不需要进行修改, 可以调用 AsNoTracking() 方法让 EF 不跟踪实体, 提高性能
全局查询筛选器
// 在实体配置类中调用 HasQueryFilter()
builder.HasQueryFilter(p => p.IsDeleted == false);
// 如果在查询时不想要应用全局查询筛选器, 调用 IgnoreQueryFilters() 方法即可
ctx.Articles.IgnoreQueryFilters().Where(p => p.IsDeleted).AsNoTracking().ToList();
在查询时如果其它列是索引, 而全局查询筛选的列不是索引, 就有可能会有性能问题
并发控制