EF Core Code First实战:一套实体模型适配SQLServer与MySQL
简介围绕EF CodeFirst代码优先的实战入门实例面向希望快速掌握Entity Framework自动建库机制的.NET开发者尤其适合需要在SQLServer与MySQL两种数据库间切换的初学者。实例完整演示了从编写数据类到自动生成数据库表的全过程并分别配置了两种数据库的连接方式便于对比理解差异。压缩包共88个文件以.cs源码、.dll程序集、.config配置、.xml文档为主同时包含.sln解决方案和.csproj工程文件包体仅13.61MB轻量易下载。项目内Program.cs与App.config等关键文件直接展现调用与配置入口结构清晰可直接打开调试运行。已有704人学习浏览适合作为Code First入门或课程设计的参考资料。内容预览显示其内置MySQL相关包如MySql.Data、EntityFramework等并附有packages配置与NuGet依赖信息可帮助读者理清环境搭建与包版本对应关系。通过本实例可直观掌握两种数据库下EF CodeFirst的配置写法减少踩坑提升数据访问开发效率。 做后端开发的朋友应该都经历过这种场景系统已经上线跑了好几个月客户突然说“我们这边用的不是 SQLServer是 MySQL能迁过去吗”。如果项目用的是“先建表、再写代码”的数据库优先模式整个数据层基本要推翻重来。但如果你一开始就用 EF Code First代码优先这个需求会变得没那么吓人。这篇文章分享我最近完成的一个实例用同一套 EF Core 实体模型分别跑通 SQLServer 和 MySQL涉及实体设计、DbContext 配置、Provider 切换、迁移生成和双库并行时的踩坑记录。适合理清 Code First 原理的后端新手也适合正在做“一套代码适配多数据库”的 .NET 开发者。1. 先搞清楚EF代码优先到底解决了什么问题1.1 从数据库优先到代码优先的思维转变传统的开发模式是数据库优先DBA 建表、写索引、定外键然后我们再根据表结构去写实体类、写仓储。这个模式没什么硬伤唯一的麻烦是——数据库结构成了项目里的“老大哥”每次改动都要先走一遍建表脚本然后同步实体类稍不留神就漏掉一个字段。代码优先把这种关系倒了过来。实体类成了“唯一事实来源”Single Source of Truth迁移脚本Migration负责把代码里的模型变化翻译成 DDL再应用到数据库。改字段就是改代码加表就是加 DbSet执行一条命令就能自动生成对应的 ALTER TABLE 或 CREATE TABLE。用我做过的一个成绩管理系统来打比方以前是“先有档案柜再按格子塞文件”现在是“先设计表格模板档案柜按模板自动定制”。后者明显更适合需求经常变化的项目而且代码评审时能直接看模型。更关键的是迁移文件是可追溯的每次数据库变更都有对应的 Git 记录比散落在团队聊天记录里的 SQL 脚本靠谱太多了。1.2 为什么我非要把SQLServer和MySQL塞进同一个项目很多团队的情况是开发环境用 SQLServer上了生产环境客户要求换成 MySQL或者集团公司内部一个项目要交付给不同客户各家的数据库标准不统一。所以从架构层面让“一套实体模型适配多种数据库”并不是炫技而是实打实的降本增效需求。EF Core 在这方面给了我们很大的空间。它的底层抽象做得不错同一个 LINQ 查询会被翻译成不同数据库的方言理论上你只要切换UseSqlServer和UseMySql再各自生成迁移文件就能让一套领域模型跑在两种数据库上。当然“理论上”三个字背后藏着不少细节这也是本篇文章重点展开的部分。2. 环境准备与依赖选型2.1 开发环境和数据库安装建议做这个实例之前先把开发环境备齐。我用的技术栈是 .NET 8 EF Core 8IDE 用 Visual Studio 2022日常用 Rider 的也完全可以。数据库方面SQLServer 建议装一张开发版或 Express 版安装教程网上很多核心是记住实例名和登录方式MySQL 我强烈推荐用 Docker 装比在本机装一堆服务干净利落docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASESchoolDb \ mysql:8.0跑完这条命令MySQL 8.0 就在本机的 3306 端口等着你了用 Navicat 或 MySQL Workbench 连上去即可。SQLServer 如果非要用 Docker 也不是不行只是内存占用大开发机配置一般的话还是建议原生安装。这里务必要养成一个习惯把数据库版本记清楚。后面配置ServerVersion时MySQL 5.7 和 8.0 的处理方式有差异别拿着 8.0 的配置去连 5.7 的库。2.2 NuGet包怎么选EF Core 连接到不同数据库靠的是 Provider 包这一步选不好后面全是坑。SQLServer 端没有悬念用微软官方的包Microsoft.EntityFrameworkCore.SqlServerMySQL 端就要多说两句了。市面上有两个主流的 Provider包名维护方我的评价MySql.EntityFrameworkCoreOracle 官方更新滞后坑比较多不推荐Pomelo.EntityFrameworkCore.MySql社区 Pomelo 团队更新及时和 EF Core 版本对齐推荐我自己一直用 Pomelo它和 EF Core 的版本要严格对应EF Core 8 对应Pomelo.EntityFrameworkCore.MySql8.xEF Core 9 对应 9.x。版本对不上大概率会在运行时爆出奇怪的映射错误。最后别漏了命令行工具包如果还没安装过先装dotnet tool install --global dotnet-ef这个工具是生成迁移、更新数据库的必备武器没有它写代码优先项目等于少了一条腿。3. 从零搭建EF Code First项目3.1 实体类与DbContext的设计要点我以学生成绩表为例建两个实体Student和Score这几乎是教学级的标准模型但足够把主要知识点都带出来。public class Student { public int Id { get; set; } public string Name { get; set; } string.Empty; public string? StudentNo { get; set; } public DateTime EnrollmentDate { get; set; } public bool IsActive { get; set; } true; public ListScore Scores { get; set; } new(); } public class Score { public int Id { get; set; } public int StudentId { get; set; } public Student? Student { get; set; } public string CourseName { get; set; } string.Empty; public decimal ScoreValue { get; set; } public DateTime ExamDate { get; set; } }实体类很简单重点在DbContext里怎么配置约束。写代码优先项目最忌讳的是“把实体丢给 EF其余全交给默认约定”。默认约定能应付简单场景但只要字段类型稍微特殊一点SQLServer 和 MySQL 生成的 DDL 就会有出入。所以我建议都用 Fluent API 显式声明一遍关键字段public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetStudent Students SetStudent(); public DbSetScore Scores SetScore(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityStudent(e { e.Property(p p.Name).HasMaxLength(50); e.Property(p p.StudentNo).HasMaxLength(20); e.HasIndex(p p.StudentNo).IsUnique(); }); modelBuilder.EntityScore(e { e.Property(p p.CourseName).HasMaxLength(100); e.Property(p p.ScoreValue).HasPrecision(5, 2); e.HasIndex(p new { p.StudentId, p.CourseName }).IsUnique(); }); } }注意两处一是HasMaxLength字符串如果不指定长度SQLServer 默认映射成nvarchar(max)MySQL 默认映射成longtext。这两种类型在索引和排序上都有麻烦所以业务字段一律给显式长度。二是HasPrecision(5, 2)指定decimal的精度否则两种数据库默认的 decimal 映射不一致后面会展开讲。3.2 多Provider切换的核心配置要让项目在两种数据库之间切换核心思路是把数据库类型和连接字符串都放进配置文件里启动时按配置注入不同的 Provider。{ DbType: SqlServer, ConnectionStrings: { SqlServer: Serverlocalhost;DatabaseSchoolDb;User Idsa;PasswordYourStrong!Passw0rd;TrustServerCertificateTrue;, MySql: Serverlocalhost;Port3306;DatabaseSchoolDb;Userroot;Passwordroot123;Charsetutf8mb4;SslModeNone; } }然后Program.cs里这样注册var builder WebApplication.CreateBuilder(args); var dbType builder.Configuration.GetValuestring(DbType); builder.Services.AddDbContextAppDbContext(options { if (dbType SqlServer) { options.UseSqlServer(builder.Configuration.GetConnectionString(SqlServer)); } else if (dbType MySql) { options.UseMySql( builder.Configuration.GetConnectionString(MySql), ServerVersion.AutoDetect(builder.Configuration.GetConnectionString(MySql)), mysql mysql.EnableRetryOnFailure()); } });这里两个点值得注意。第一ServerVersion.AutoDetect是 Pomelo 提供的方便方法会自动去查一下服务器版本代价是启动时多一次网络开销如果你明确知道自己连的是 MySQL 8.0也可以直接写死new MySqlServerVersion(new Version(8, 0, 36))省掉自动探测那一步。第二EnableRetryOnFailure是 Pomelo 的执行策略默认处理临时性连接故障建议加上否则服务器重启的瞬间应用可能直接抛异常。4. 实操让同一套实体跑通两种数据库4.1 生成并应用SQLServer迁移项目能跑起来、能连上 SQLServer 之后第一步是生成迁移。很多人喜欢一个迁移目录走天下但当你目标数据库不止一种时强烈建议按 Provider 分目录否则哪天混合应用迁移可能把 SQLServer 的迁移脚本误打到 MySQL 上。我的做法是这样dotnet ef migrations add InitSqlServer \ --context AppDbContext \ --output-dir Migrations/SqlServer执行完会在Migrations/SqlServer目录下生成带时间戳的迁移文件。打开看一眼里面全是 SQLServer 方言的 DDL比如主键用IDENTITY字符串用nvarchar(50)。这一步能帮你快速发现模型设计的问题如果某个字段生成了预期之外的类型趁现在改模型不要拖到上线。然后应用迁移dotnet ef database update \ --context AppDbContext \ --connection Serverlocalhost;DatabaseSchoolDb;User Idsa;PasswordYourStrong!Passw0rd;TrustServerCertificateTrue;成功后用 SSMS 刷新一下能看到多了一张__EFMigrationsHistory表。这张表是 EF 自己的“版本履历表”记录了当前数据库已经应用了哪些迁移后续再执行database update时EF 会跳过历史表里已有的迁移。很多人第一次看到这张表会误以为是系统表千万不要删删了之后 EF 会认为所有迁移都没应用过执行 update 时会尝试重建表到时候等着你的就是连环报错。4.2 切换MySQL并生成第二个迁移SQLServer 跑通之后把配置文件里的DbType改成MySql这时候直接启动应用大概率还能正常连库但没有表。别急先给 MySQL 生成一套专属于它的迁移dotnet ef migrations add InitMySql \ --context AppDbContext \ --output-dir Migrations/MySql注意我特意用了不同的迁移名称InitMySql而不是沿用InitSqlServer。这是踩坑踩出来的经验EF 的迁移历史表只认MigrationId不区分 Provider如果你给两个数据库生成同名迁移切到 MySQL 时 EF 会误以为迁移已经应用过。用不同名称虽然看起来不那么整齐但换来了跨库的稳定。执行完打开 MySQL 的迁移文件你会发现同样的实体类生成的 DDL 已经完全变了主键变成了int AUTO_INCREMENT字符串变成了longtext或varchar(50)IsActive映射成了tinyint(1)。这就是 Provider 切换的意义——你写的 LINQ 和实体类不变底层 SQL 方言和字段类型全由 Provider 接管。接下来应用迁移命令和 SQLServer 版本几乎一样只换连接字符串dotnet ef database update \ --context AppDbContext \ --connection Serverlocalhost;Port3306;DatabaseSchoolDb;Userroot;Passwordroot123;Charsetutf8mb4;SslModeNone;完成之后用 Navicat 或 MySQL Workbench 连上去看一下表结构、索引、历史表都在和 SQLServer 侧一一对应。4.3 写入种子数据验证跨库一致性表建好了先别急着写业务接口手动插几行数据验证一下最踏实。我习惯在OnModelCreating里用HasData配种子数据这样每次迁移部署后都会自动做一次数据初始化modelBuilder.EntityStudent().HasData( new Student { Id 1, Name 张三, StudentNo S001, EnrollmentDate new DateTime(2024, 9, 1), IsActive true }, new Student { Id 2, Name 李四, StudentNo S002, EnrollmentDate new DateTime(2024, 9, 1), IsActive true } );有一点需要提醒种子数据的HasData在两种数据库下有细微差异。SQLServer 的大多数类型都能直接映射而 MySQL 对DateTime、decimal的处理更敏感所以种子数据的精度要写清楚比如decimal最好写成50.00m而不是50m否则迁移时可能出现类型转换告警。5. 双数据库并行最容易踩的坑5.1 类型映射差异decimal、DateTime、bool这是跨库项目中“看起来能跑、跑起来就出妖”的重灾区。同一套实体类在两个数据库里生成的物理类型完全不同。举几个实际例子实体属性SQLServer 默认映射MySQL 默认映射Pomelodecimal ScoreValuedecimal(18,2)decimal(65,30)DateTime ExamDatedatetime2datetime(6)bool IsActivebittinyint(1)如果你不显式配置HasPrecision同一个ScoreValue在 SQLServer 里是decimal(18,2)在 MySQL 里却变成decimal(65,30)。sqlserver 里算成绩保留两位小数没问题MySQL 里你会看到一长串诡异的小数尾巴就是因为精度映射不一致。所以模型里凡是涉及金额、分数、比率这种对精度敏感的字段老老实实写HasPrecision(18, 2)或者HasPrecision(5, 2)别再依赖默认约定。DateTime也有坑。SQLServer 的datetime2精度是 100 纳秒MySQL 的datetime(6)精度是微秒相差一个数量级。如果你的业务逻辑里有“拿时间戳做唯一标识”这种危险操作在两个库上表现会不同。更保险的做法是日期时间字段统一在应用层用DateTime.UtcNow赋值不要用数据库的GETDATE()或NOW()这样至少能做到跨库行为一致。bool映射的差异则体现在查询和存储两方面。SQLServer 的bit只有 0 和 1MySQL 的tinyint(1)其实是个整数类型如果用原生 SQL 注入可能会写入 2、3 这样的值等 EF 读出来的时候IsActive就变成true了。这也解释了为什么我强烈建议所有写操作都走 EF不要混用裸 SQL除非你有充分的理由并做了严格校验。5.2 索引命名与迁移历史表问题跨库项目里索引和约束的命名默认策略也不同。SQLServer 默认的索引名格式是IX_表名_字段名MySQL 里也差不多但长度上限和字符集规则不一样。当你的索引名超过 MySQL 的限制或者包含特殊字符时迁移阶段就会直接报错。这里有一个非常实际的经验在OnModelCreating里给索引显式命名尤其是联合索引。这样既能保证两种数据库的索引名一致方便后续排查慢 SQL也能避免默认命名在换库时出现兼容性问题。迁移历史表__EFMigrationsHistory在两种数据库里的表现也不完全一样。SQLServer 里它是普通表MySQL 里它默认是MyISAM或InnoDB取决于数据库版本和配置。如果并行连接两个库我建议定期检查这张表里的MigrationId是否和应用代码中的迁移类一一对应。最简单的方法是打开dotnet ef migrations list --context AppDbContext它会列出应用到当前连接数据库的迁移列表和代码里的迁移对比一下一目了然。5.3 连接字符串与SSL踩坑连接字符串是最容易被低估的坑。SQLServer 侧本地开发经常遇到“证书链是由不受信任的颁发机构颁发的”这是因为默认开启了加密连接。如果你在本地开发环境不需要加密还是在连接字符串末尾加上TrustServerCertificateTrue省心。MySQL 侧连接字符串里两个参数要特别注意。一是Charsetutf8mb4不设置的话默认字符集可能不是 utf8mb4中文写入后会出现乱码包括表情符号直接丢失。二是SslModeNone本地开发时如果不开这个Pomelo 可能因为 SSL 握手超时连不上数据库生产环境则建议改成SslModeRequired别一刀切。我见过很多人在“本机能连 SQLServer、MySQL 却连不上”的问题上卡半天最后排查半天是Port没写对。MySQL 默认端口是 3306但只要改过 Docker 映射就未必是这个值。连接字符串里显式写Port3306是最稳妥的千万别省。5.4 原生SQL与批量操作注意事项EF Code First 下推荐用 LINQ但总有一些场景绕不开原生 SQL比如复杂报表、特殊性能优化。这时你要清楚原生 SQL 是绕过模型的一旦数据库结构变化SQL 可能不会跟着同步更新。我的建议是跨库项目里原生 SQL 尽量用在“只读”场景写操作一律走 EF。如果非得写原生 SQL避免两个库的方言差异。比如 sqlserver 里字符串转数字常用CAST(123 AS INT)MySQL 里也能用CAST但行为略有不同SQLServer 遇到空字符串转数字会抛异常MySQL 会更宽容地返回 0。这种微妙的差异跨库时最容易咬人。批量操作方面EF Core 8 引入了ExecuteUpdate和ExecuteDelete可以直接执行批量删除和更新不再需要先查询出来再逐条改。这两个 API 在 SQLServer 和 MySQL 上都有对应的原生翻译性能差异不大。但要注意批量操作不会触发SaveChanges的ChangeTracker逻辑也不会自动更新导航属性如果你后续还要用这些实体做别的操作建议先让ChangeTracker.Clear()清空跟踪状态。6. 常见问题排查速查表顺手整理一份速查表都是我在这个实例里实际碰到过的问题。遇到对应现象时可以按图索骥。现象常见原因解决方案MySQL 连不上报Unable to connect to any of the specified MySQL hostsMySQL 没启动 / Docker 端口没映射 / 连接串 Port 错误用docker ps确认容器状态检查端口映射SQLServer 报证书错误未启用 TrustServerCertificate连接串加TrustServerCertificateTruePomelo 报版本不匹配Provider 包版本和 EF Core 版本不对应Pomelo 包版本和Microsoft.EntityFrameworkCore大版本保持一致迁移时报“数据丢失警告”模型改动和已有迁移不一致执行dotnet ef migrations list核对迁移列表必要时回滚插入中文变成?MySQL 字符集不对连接串加Charsetutf8mb4建库时指定 utf8mb4日期时间精度不一致未显式配置精度对DateTime不默认用DateTime.UtcNow统一赋值decimal 小数尾巴异常默认精度映射不同用HasPrecision(18, 2)显式声明删除__EFMigrationsHistory后更新失败历史表数据丢失不要手动删必要时手动构造迁移记录或用脚本重建这个实例最大的收获不是“我会用 EF Code First 了”而是理解了 EF Core 的迁移机制和 Provider 抽象之间的关系。迁移不只是数据库结构的快照它绑定了具体的数据库方言。切换数据库时实体类可以不变但迁移必须重新生成。这也解释了为什么“一套代码适配多数据库”听起来很美实际操作中要为每个数据库维护独立的迁移历史。如果你正准备在公司项目里做类似的多数据库兼容我的建议是尽早把数据库类型、连接字符串、迁移目录、迁移命名这些规则定好。定得越早后面切换越轻松。等到数据库表已经三四十张再回头补迁移和梳理 Provider 差异那才叫一个痛苦。本文还有配套的精品资源点击获取