Entity Framework和Entity Framework Core是.NET框架中用于处理数据库操作的两个常用框架。它们都提供了一种简单、高效的方式来与数据库进行交互,并支持多种数据库类型(如SQL Server、MySQL、PostgreSQL等)。
Entity Framework是一个ORM(对象关系映射)框架,它允许开发人员使用面向对象的方式操作数据库。它提供了一组API来定义实体类、映射到数据库表、执行CRUD(创建、读取、更新、删除)操作等。
Entity Framework Core是Entity Framework的最新版本,它是一个跨平台的框架,可以在多个平台上运行。它引入了一些新的特性,如依赖注入、LINQ查询语法的改进等,使得开发者可以更加方便地使用.NET Core应用程序与数据库进行交互。
EF6 是专为 .NET Framework 设计的。EF Core 是为跨平台设计的,支持 .NET Core 和 .NET 5/6+,这意味着它可以在 Windows、Linux 和 macOS 上运行。
EF6 主要使用基于约定的模型构建方式,也支持 Fluent API 和数据注释。EF Core 继续支持这些方式,并且更加注重 Fluent API 的使用,使其成为主要的模型定义方式。
先决条件
安装Nuget包
Install-package Microsoft.EntityFrameworkCore.Tools
Install-package Microsoft.EntityFrameworkCore.Design
Install-package Microsoft.EntityFrameworkCore.SqlServer
建议采用手工安装,指定版本6.0.10
一、模型设计模式
微软自设计EF以来,提供了以下几种设计模式:
序号 | 设计模式 | 模式简称 | 模式描述 |
---|---|---|---|
1 | DataBase First | 数据库优先 | 通过VS设计器读向生成Model类 |
2 | Model First | 模型设计优先 | 通过[ADO.NET实体数据模型]设计器生成可视模型,再通过设计器完成对Model类以及数据库表结构生成 |
3 | Code First | 代码优先 | 通过代码编写Model,结合fluent API进行映射关系配置,同步更新到数据库及表结构等 |
实际上CodeFirst有两种用法:
一种呢有数据表结构时,先通过[DataBase First]模式的VS设计器或者数据库上下文脚手架[Scaffold-DbContext]逆向生成Model类及上下文实体映射关系;然后增、删、改Model类属性时,通过官方提供的[Migrations]迁移工具,进行更新数据库实体结构。
另一种就是空CodeFirst纯手工编写Entity实体类以及DbContext上下文映射关系配置,再调用[Migrations]迁移工具,进行更新数据库实体结构。
1、Database First模式
前提条件:必须已创建数据库及表实体结构。
参数详解:
-Connection <String> 用于连接到数据库的连接字符串。 对于 ASP.NET Core 2.x 项目,该值可以是连接字符串>的名称=<名称。 在这种情况下,名称来自为项目设置的配置源。 这是一个位置参数,并且是必需的。
-Provider <String> 要使用的提供程序。 通常,这是 NuGet 包的名称,例如:Microsoft.EntityFrameworkCore.SqlServer。 这是一个位置参数,并且是必需的。
-OutputDir <String> 要在其中放置实体类文件的目录。 路径相对于项目目录。
-ContextDir <String> 要在其中放置 DbContext 文件的目录。 路径相对于项目目录。
-Namespace <String> 要用于所有生成的类的命名空间。 默认设置为从根命名空间和输出目录生成。 已在 EF Core 5.0 中添加。
-ContextNamespace <String> 要用于生成的 DbContext 类的命名空间。 注意:重写 -Namespace。 已在 EF Core 5.0 中添加。
-Context <String> 要生成的 DbContext 类的名称。
-Schemas <String[]> 要为其生成实体类型的表的架构。 如果省略此参数,则包含所有架构。
-Tables <String[]> 要为其生成实体类型的表。 如果省略此参数,则包含所有表。
-DataAnnotations 使用属性配置模型(如果可能)。 如果省略此参数,则仅使用 Fluent API。
-UseDatabaseNames 使用与数据库中显示的名称完全相同的表和列名。 如果省略此参数,数据库名称将更改为更符合 C# 名称样式约定。
-Force 覆盖现有文件。
-NoOnConfiguring 不生成 DbContext.OnConfiguring。 已在 EF Core 5.0 中添加。
-NoPluralize 请勿使用复数化程序。 已在 EF Core 5.0 中添加。
-Project <String> 目标项目。 如果省略此参数,则包管理器控制台的默认项目将用作目标项目。
(1) 生成Model类
Scaffold-DbContext 'Data Source=[服务器Ip];Initial Catalog=[数据库];User Id=[用户名];Password=[密码];integrated security=false;Encrypt=True;TrustServerCertificate=True;' Microsoft.EntityFrameworkCore.SqlServer -OutputDir [目录名] -Context [DbContext类名]
注意执行前,指定项目位置
当数据库中表发生更改时,可以直接使用下面命令在Nuget控制台中直接Entity
Scaffold-DbContext 'Data Source=[服务器Ip];Initial Catalog=[数据库];User Id=[用户名];Password=[密码]; integrated security=false;Encrypt=True;TrustServerCertificate=True;' Microsoft.EntityFrameworkCore.SqlServer -OutputDir [目录名] -Context [DbContext类名] -Force
(2) DbContext上下文动态配置
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
public partial class LMIMSContext : DbContext
{
public static readonly ILoggerFactory logger = LoggerFactory.Create(builder => { builder.AddConsole(); });
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
//根据配置文件读取数据库连接
IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
string connStr = configuration.GetConnectionString("LMIMSContext");
//使用日志记录sql语句
optionsBuilder.UseLoggerFactory(logger).EnableSensitiveDataLogging().UseSqlServer(connStr);
}
}
}
注意,需要提前配置好appsettings.json
{
"ConnectionStrings": {
"LMIMSContext": "Data Source=ServerIp;Initial Catalog=DBName;User Id=sa;Password=xxxxxx;integrated security=false;Encrypt=True;TrustServerCertificate=True;"
},
}
2、Code First模式
代码优先模式,是普遍最常用的开发设计模式。最大的特点就是,当已经存在数据库表结构及Model类时,在不毁坏或丢失数据的情况下,对实体结构进行属性的新增、删除和修改。这样即灵活又高效,极大的提高了可维护的效率。
下面以示例讲解,CodeFirst优先的设计思想和实际用途。比如,提前设计了如下Model类:
//用户
public partial class User
{
public User()
{
UsersCompanies = new HashSet<UsersCompany>();
UsersDepts = new HashSet<UsersDept>();
}
public long Id { get; set; }
public string Name { get; set; } = null!;
public string Phone { get; set; } = null!;
public bool Sex { get; set; }
public DateTime CreatedTimestamp { get; set; }
public virtual ICollection<UsersCompany> UsersCompanies { get; set; }
public virtual ICollection<UsersDept> UsersDepts { get; set; }
}
//部门
public partial class Dept
{
public Dept()
{
InverseParent = new HashSet<Dept>();
UsersDepts = new HashSet<UsersDept>();
}
public long Id { get; set; }
public string Name { get; set; } = null!;
public long? ParentId { get; set; }
public virtual Dept? Parent { get; set; }
public virtual ICollection<Dept> InverseParent { get; set; }
public virtual ICollection<UsersDept> UsersDepts { get; set; }
}
//用户部门
public partial class UsersDept
{
public long Id { get; set; }
public long UserId { get; set; }
public long DeptId { get; set; }
public virtual Dept Dept { get; set; } = null!;
public virtual User User { get; set; } = null!;
}
/// <summary>
/// 用户公司
/// </summary>
public partial class UsersCompany
{
public long Id { get; set; }
public string Name { get; set; } = null!;
public long UserId { get; set; }
public virtual User User { get; set; } = null!;
}
public class SysDbContext : DbContext
{
public SysDbContext() { }
public SysDbContext(DbContextOptions options) : base(options) { }
public virtual DbSet<Dept> Depts { get; set; } = null!;
public virtual DbSet<User> Users { get; set; } = null!;
public virtual DbSet<UsersCompany> UsersCompanys { get; set; } = null!;
public virtual DbSet<UsersDept> UsersDepts { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=IMSDataAcquire;User Id=sa;Password=pwd;integrated security=false;Encrypt=True;TrustServerCertificate=True;");
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
(1) 创建迁移及快照
语法:(PowerShell 脚本语法)
Add-Migration [InitialCreate] [-outputdir DirectoryName]
InitialCreate: 迁移工具生成的类名,注意文件名则由生成的时间+类名组成。
-outputdir DirectoryName: 指定类文件输出目录位置。
打开程序包管理器控制台,PM环境下执行【add-migration InitialCreate】语句:
执行完成后,默认会在项目根路径创建[Migrations]目录,里面存在如下内容:
xxx_InitialCreate.cs ->待迁移MigrationBuilder fluentish API脚本
xxx_InitialCreate.Designer.cs ->ModelBuilder 解释器
SysDbContextModelSnapshot.cs ->最后Migration版本快照
注意, Add创建时,系统首先检查DbContext上下文连接字符串配置尝试连接,同时检查数据库是否存在;存在则会检查是否有最近一次快照,有则比较差异进行创建,没有则直接初始化创建;
(2) 创建数据库架构或更新实体
语法:Update-Database []
每执行该语句时,会先找到最新时间点的迁移脚本类,从中进行解析写入数据库。
在执行Update-Database前,可以使用【Script-Migration】生成更新的Sql脚本,默认存放在obj\Debug\net6.0-windows位置。OK,先通过【Update-Database】更新写入数据库。其结果如下:
会发现首字字段为整型的Id,系统均会默认识别为可自增主键特性。同时Model类引用的ICollection<T>对象,均被识别为外键约束关系,且默认具备DELETE CASCADE特性。但是想直接表名、列名注解、字段类型及长度、手工指定一对一、一对多,甚至是多对多关系等映射特性,Fluent API如何配置来实现?
其实,通过DbContext上下文重写OnModelCreating方法,比如对User类进行增、删、改,并对它重写映射特性:
//重写DbContext上下文类:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(entity =>
{
//set Primary Key
//entity.HasKey(e => e.Id)
//.HasName("PK_UserIdKey");
entity.ToTable("Users");
entity.HasComment("用户表");
entity.Property(e => e.Name)
.HasMaxLength(20)
.IsUnicode(false)
.HasComment("用户名");
entity.Property(e => e.CreatedTimestamp)
.HasColumnType("datetime")
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间");
});
modelBuilder.Entity<Dept>(entity =>
{
entity.ToTable("Depts");
entity.HasComment("部门表");
entity.Property(e => e.Name)
.HasMaxLength(20)
.IsUnicode(false)
.HasComment("部门名");
entity.HasIndex(e => e.ParentId, "IX_Depts_ParentId");
entity.HasOne(d => d.Parent)
.WithMany(p => p.InverseParent)
.HasForeignKey(d => d.ParentId);
});
modelBuilder.Entity<UsersDept>(entity =>
{
entity.HasIndex(e => e.DeptId, "IX_UsersDepts_DeptId");
entity.HasIndex(e => e.UserId, "IX_UsersDepts_UserId");
entity.HasOne(d => d.Dept)
.WithMany(p => p.UsersDepts)
.HasForeignKey(d => d.DeptId);
entity.HasOne(d => d.User)
.WithMany(p => p.UsersDepts)
.HasForeignKey(d => d.UserId);
});
modelBuilder.Entity<UsersCompany>(entity =>
{
entity.HasIndex(e => e.UserId, "IX_UsersCompanys_UserId");
entity.HasOne(d => d.User)
.WithMany(p => p.UsersCompanies)
.HasForeignKey(d => d.UserId);
});
base.OnModelCreating(modelBuilder);
}
重新执行【Add-Migration UpdateEntity】创建更新实体迁移脚本:
此时,就可以在xxxxx_UpdateEntity脚本类发现,Up方法体,系统自动比较快照的差异,增加需要增加的属性及相关特性,而Down方法体恰恰相反,去生成要移除的属性及相关特性。说简单点,就是Sql脚本中,先判断其对象是否存在,存在则先删除,然后再创建。
值得注意的是,当快照记忆的数据库实体和当前调整过的实体类存在外键约束关系时,很可能造成新生成的迁移脚本,无法Update-Database成功!那是由于数据库实体已经和当前产生的快照存在不一致性的问题(比如,手工删除xxxxxxSnapshot快照类重新生成过,或手工去调整过数据库实体结构造成的差异)直接都将造成Migration无法通过快照检查出和数据库实体的差异性。
注意,可以通过【Drop-Database】命令删除原有数据库,但要确保数据库是非开发用的测试库,非生产环境使用库。
生成结果:
直接注意的是,在[__EFMigrationsHistory]表,会记录每一次通过Update_Database执行成功的Migration脚本类文件及产品版本,记录到该表中:
(2) 删除迁移
语法:
Remove-Migration
删除迁移直接调用,则只能删除最新的Migration脚本类。但如果已经应用生效(已生成数据库实休),则会提示上述错误。
The migration '20240309063759_UpdateEntity' has already been applied to the database. Revert it and try again. If the migration has been applied to other databases, consider reverting its changes using a new migration instead.
大译就是,只有未Update-Database过的Migration脚本类是完全可以删除。那删除已经应用到数据库的迁移,则可以进行【迁移回滚】,比如我已经执行了3次迁移脚本,回滚到上一个版本:
再次执行删除,则文件就被删除了。所以微软是建议通过这种迁移脚本文件版本的方式进行管理实体的改动和采用事务回滚的方式进行实体状态的还原,还是挺安全和便捷的。
(3)模型排除
在DbContext上下文,OnModelCreating方法体中实现。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Dept>(entity =>
{
//迁移冲突时,可排除模型
entity.ToTable("UserDept", t => t.ExcludeFromMigrations());
});
}
最后,笔者总结一下实体迁移的感觉吧,如果表结构仍需保留,用此法完全可行。但如果要通过它维护实体结构的移除那就不管用了,需要手工去删除Db实体和模型类。包括重构整个数据库,最好还是手工维护的好,虽说Remove-Database可以移除,但在重建时,仍存在一定的报错。不知道是否我测试的还存在问题,也请各位大咖指正。