Entity Framework Core#
1. 导航属性#
// A |->* B
public class A{
//...
public List<B> Bs{get;set;}
}
public class B{
// ...
public B B{set;get;}
}
导航属性可以是任何列表类型。
2. 数据库上下文 ⚠️#
用来协调 EF 和数据库的管理类。#
- 一个 Context 是一个继承自 DbContext 的非抽象类。这个类的功能首先是公开所有实体的集合,操作这些集合可以在存储中读取或保存数据,除此之外 Context 保存数据的模型。在使用前需要正确配置各个实体的模型以及其他必要的配置。
- Context 的另一个重要的功能就是跟踪对实体的更改,以便我们在保存更改时,它知道该如何处理。
- Context 跟踪的每个实体所处的以下状态之一:未更改 /added,已修改 /modified,已添加 /added,已删除 /deleted或已分离 /detached。可以将上下文视为沙箱,我们可以在其中对实体集合进行更改,然后通过一次保存操作应用这些更改。
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
// 实体集 DbSet 对应数据库表
// 由于Courses表包含了 下面两个表,因此,即使不写下方两表,EF也会自动包含
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
// 覆盖默认行为,设定表名
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
}
}
}
DbContext 钩子方法#
- OnConfiguring 方法:当 context 需要配置它的 provider 和 链接字符串的时候,该方法会被调用,这为开发者提供了介入的入口。
- OnModelCreating 方法:在 EF Core 组装数据模型时会被自动调用,你将会在这个方法里添加构建模型的代码。
- SaveChanges 方法:当我们希望将修改保存到数据库时进行调用。 返回为受保存操作影响的记录数。
Database Provider / 数据库提供程序#
- EF core 是数据库无关的。因此不同的数据库需要为其提供特定接口
- 引入相关数据库提供程序包,并在 Context 的 OnConfiguring 中添加对应配置方法(UseSqlServer)
3. 服务注册#
// Startup.cs
/* Using */
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
/* Start up */
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<SchoolContext>(
options=>options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllersWithViews();
// 开发环境 异常筛选功能
services.AddDatabaseDeveloperPageExceptionFilter();
}
// appsetting.json
{
"ConnectionStrings": {
"DefaultConnection": "数据库字符串"
},
}
4. 创建数据模型#
-
创建数据模型需要 实体 Entity 类型。实体类型就是一系列 C# 类,通常与数据库对应。
-
配置 实体类型 与 数据库实体 之间的关系。有两种方式:”fluent API“ 和 ” 数据注释 “
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; // 配置 对应表名 和 对应数据库架构名 [Table("blogs",Schema="dbo")] public class Blog{ [Key] // 主键,通常efcore会自动识别 public int BlogId{set;get;} [MaxLength(400)] // 最大长度 public string Url{set;get;} [NotMapped] // 排除映射 public DataTime LoadedFromDatabase {set;get;} [Column("rating",TypeName="decimal(5,2)")] // 映射列名和类型 public decimal Rating {set;get;} } // 使用 fluent API 配置默认架构名 protected override void OnModelCreating(ModelBuilder modelBuilder){ modelBuilder.HasDefaultSchema("dbo"); }
“实体引用 / 引用导航” 配置 一对一#
//一Blog 对应 一个 Customer
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<BlogImage> BlogImages { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//设置 Blog与BlogImage 一对一关系
modelBuilder.Entity<Blog>()
.HasOne(b => b.BlogImage)
.WithOne(i => i.Blog)
.HasForeignKey<BlogImage>(b => b.BlogForeignKey);
}
}
public class Blog
{
public Blog()
{
Posts = new HashSet<Post>();
}
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public int? ImageId { get; set; }
// 引用导航 efcore 可以自动配置 写法见官方文档
public BlogImage BlogImage { get; set; }
public HashSet<Post> Posts { get; set; }
}
public class BlogImage
{
public int BlogImageId { get; set; }
public string ImageUrl { get; set; }
public string Caption { get; set; }
}
“实体集合 / 集合导航” 配置 :一对多#
//实体集合属性的配置
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//配置 Blog 的一对多关系
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId);
}
}
public class Blog
{
public Blog()
{
// 因为有 HashSet 导航,构造函数要添加此内容
Posts = new HashSet<Post>();
}
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public int? ImageId { get; set; }
public BlogImage BlogImage { get; set; }
// 集合导航,表示一对多关系,需要HashSet辅助
public HashSet<Post> Posts { get; set; }
}
public class Post
{
[Key]
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
总结#
数据模型的作用除了说明数据结构,还需要说明数据关系。关系也是数据库的核心。
EF core 支持多种方式描述模型关系,使用注释是最直观简单的方法,特殊的关系还需要使用 FluentAPI 辅助。
但两者都是在 ModelBuilder 中配置相关行为。
5. 迁移 从模型到数据库#
迁移是使用 EF core 迁移工具从实体模型创建和管理数据库。
迁移过程#
-
按照上述步骤,构建实体模型和关系,会有一个 的类。通常是你打算的数据库名称 + Context,这个类继承与 DbContext。EF core 正是依据这个文件中的实体模型和关系说明来生成数据库的。
-
打开终端
-
首先确定安装 .NET core CLI 工具
# 全局安装 dotnet-ef 工具 dotnet tool install --global dotnet-ef # 更新 dotnet tool update --global dotnet-ef # 为 项目添加 EFCoreDesign 包 dotnet add package Microsoft.EntityFrameworkCore.Design # 也可以使用Nuget管理器 # 检查 工具安装情况 dotnet ef
⚠️ EF Core 工具参考 (.NET CLI) - EF Core | Microsoft Learn 工具的详细使用方法见此处
-
在工程目录下打开终端:
# 创建一个迁移 dotnet ef migrations add InitialCreate ## 该命令会生成一个 migration 文件夹,用来记录迁移,也可以直接修改其内容。 # 创建数据库架构 dotnet ef database update
⚠️ 关于验证问题,可以在连接字符串中添加
TrustServerCertificate=true;Encrypt=true
-
Done.
发展模型#
当你的实体模型发生改变,我们需要重新更新数据库。
首先必须要明确,迁移工具生成的 migrations/ 系列文件,是迁移记录,是数据库结构演进的完整说明。它包含了一份数据库结构快照,和一份迁移文件。
因此,当模型改变时,我们可以手动更改 migrations/ 文件,并再次执行dotnet ef database update
来应用迁移。
为了保留数据库演进的记录,我们使用以下流程:
-
创建新的迁移
# 这里最好使用有意义的迁移命名 dotnet ef migrations add AddSomeTable
-
应用迁移
dotnet ef database update
说明#
阅读 migration/ 文件夹,里面有一系列带时间戳和迁移名称的.cs
文件,用来指示每次迁移执行的动作。可以看出,当实体模型改变时,EFcore 会自动比较并进行增量更新,而非重新生成数据库。
另一个文件是数据库快照的.cs
文件,可以看出里面是真实的数据库表实体模型。是上一次更新有的数据库快照。
两种文件都是 CSharp 文件,可以加入到版本管理中,从而记录工程开发过程中的数据库演进。
6 其他#
排除类型#
若当前 Context 引用了其他 Context 中的类型,会在迁移时出现冲突(引用的对象在当前 Context 并不存在,出现错误是正常的),此时要从 Context 中排除该类型。
// 这是引用Context类
// 在模型创建钩子中,排除不存在的类型。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdentityUser>()
.ToTable("AspNetUsers", t => t.ExcludeFromMigrations());
}
列重命名#
EFCore 无法区分列的重命名或删除,因此,当你重命名列时,EFcore 会生成 Delete-Create 的操作。这可能会导致数据丢失。需要手动更改。
// EFcore 生成的迁移动作
migrationBuilder.DropColumn(
name: "Name",
table: "Customers");
migrationBuilder.AddColumn<string>(
name: "FullName",
table: "Customers",
nullable: true);
// 需要将其修改为
migrationBuilder.RenameColumn(
name: "Name",
table: "Customers",
newName: "FullName");
在迁移中引入 SQL 行为#
假设要将实体中原来的 Firstname 和 LastName 合并成 FullName 重新存储,并删除前两个列。
EFcore 不会自动识别这个行为,需要手动引入 SQL 语句用来迁移。
migrationBuilder.AddColumn<string>(
name: "FullName",
table: "Customer",
nullable: true);
migrationBuilder.Sql(
@"
UPDATE Customer
SET FullName = FirstName + ' ' + LastName;
");
migrationBuilder.DropColumn(
name: "FirstName",
table: "Customer");
migrationBuilder.DropColumn(
name: "LastName",
table: "Customer");
⚠️ 此流程是有顺序的。
当要利用旧数据进行迁移时,可以使用 SQL
自定义 SQL 迁移#
可以在不改变模型的情况下,生成一个空迁移,在手动填充 SQL 语句。
SQL 可以操作模型不知道的数据库对象。
migrationBuilder.Sql(
@"
EXEC ('CREATE PROCEDURE getFullName
@LastName nvarchar(50),
@FirstName nvarchar(50)
AS
RETURN @LastName + @FirstName;')");
⚠️ 当某个语句必须是 SQL 批处理中的第一个或唯一一个语句时,请使用 EXEC
。 它还可以用来解决幂等迁移脚本中的分析程序错误,当表中当前不存在引用的列时,可能会发生此类错误。
⚠️ 通常 EFCore 使用事务执行迁移操作。当某些数据库不支持该操作时,可以向migrationBuilder.Sql
中传入参数suppressTransaction: true
来选择不使用事务。
7. 迁移管理#
删除迁移#
使用 dotnet ef migrations remove
来移除 上一个迁移
⚠️ 请勿移除已应用的迁移!
列出迁移#
dotnet ef migrations list
列出所有迁移
重置所有#
直接删除 Migration 文件夹即可删除所有迁移记录,详情:
管理迁移 - EF Core | Microsoft Learn
应用迁移#
-
生成 SQL 脚本,从零开始生成数据库。这是将工程模型部署到生产环境的最佳实践。
dotnet ef migrations script [from] [to]
From to 分别是迁移版本名称。to 默认是最新。可以使用上述代码回退。
-
自动比较 SQL 脚本。对于将迁移应用在现有数据库,但并不清楚其当前版本的情况下。efcore 会检索迁移历史记录,自动更新。
dotnet ef migrations script --idempotent
-
使用命令行工具升级数据库,这是开发环境测试环境的最佳实践
dotnet ef database update [to]
-
程序运行时执行迁移,这是本地开发测试是的最佳实践,可以省去每次启动都手动迁移的步骤。
// 在 .net 运行主函数中添加 var host = CreateHostBuilder(args).Build(); using(var scope = host.Services.CreateScope()){ var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); younameContext.Database.Migrate(); // 这句是关键。 } host.Run();
-
创建捆绑包
团队管理#
⏰