EF Core代码优先双数据库实战:SQLServer与MySQL迁移避坑指南
简介这份资源面向正在学习或使用 Entity Framework 的 .NET 开发者重点演示 Code First「代码优先」模式在 SQLServer 与 MySQL 两种数据库下的完整落地方式。开发者只需编写领域模型数据类即可由 EF 自动生成数据库结构适合想对比不同数据库提供程序配置差异、理清迁移与连接字符串写法的初中级人员参考。压缩包共 88 个文件约 13.61MB以 cs 源码、dll 程序集、config 配置、xml 文档为主另含 exe、nupkg、csproj 与 sln 等工程与依赖文件两个示例项目分别对应 SQLServer 与 MySQL 场景并引入 MySql.Data.Entity、EntityFramework 等包。目前已有 706 人学习下载。通过该实例可直观看到同一套模型代码在两种数据库下的工程组织、包引用与配置差异便于快速搭建可运行的双数据库 Code First 演示环境对照排查连接与映射问题。1. EF代码优先实例从C#类到两种数据库的落地路径很多团队在项目初期用 SQL 脚本建表改一版需求就要同步改脚本、改文档、改实体类三处对不上就等着上线翻车。EF 代码优先Code First换了个思路C# 类就是唯一事实来源数据库结构由模型推导出来。标题里的“包含 SQLServer 与 MySQL”说的不是二选一而是同一套模型要能同时落到两种数据库上——这在多客户交付、信创适配、本地开发用轻量库而生产用商用库的场景里非常常见。这篇笔记面向已经会写 C#、但还没把 EF 代码优先在双数据库上跑通的开发者从模型定义、迁移生成、Provider 差异一路讲到参数配置和踩坑记录目标是让你照着能复现一套可切换数据库的最小工程。2. 模型先行实体类、DbContext 与双 Provider 的选型理由代码优先的核心不是“不用写 SQL”而是把数据库结构的定义权收回到 C# 代码里。EF Core 通过读取实体类的属性、特性和 Fluent API 配置推导出表名、列类型、主键、外键和索引。理解这条推导链才能明白为什么同一个模型在 SQLServer 和 MySQL 上生成的 DDL 会不一样。2.1 实体类与 Fluent API 的分工实体类负责表达“有哪些字段”Fluent API 负责表达“字段在数据库里长什么样”。两者分开的好处是实体类保持干净不堆数据库特性数据库相关的约束集中在一个地方换 Provider 时只改配置不改业务类。// Models/Order.cs public class Order { public int Id { get; set; } // 主键默认自增 public string OrderNo { get; set; } ; // 订单号需唯一索引 public decimal Amount { get; set; } // 金额注意精度 public DateTime CreatedAt { get; set; } // 创建时间 public int CustomerId { get; set; } // 外键 public Customer? Customer { get; set; } // 导航属性 } public class Customer { public int Id { get; set; } public string Name { get; set; } ; public ListOrder Orders { get; set; } new(); }上面这段代码里Id会被约定为主键CustomerId会被识别为外键Customer是导航属性。这些是 EF Core 的默认约定不需要额外配置就能工作。但decimal的精度、string的最大长度、唯一索引这些默认约定给不出合理值必须用 Fluent API 补。// Data/AppDbContext.cs public class AppDbContext : DbContext { public DbSetOrder Orders SetOrder(); public DbSetCustomer Customers SetCustomer(); public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { builder.EntityOrder(e { e.ToTable(t_order); // 显式指定表名 e.Property(p p.OrderNo) .HasMaxLength(32) .IsRequired(); e.HasIndex(p p.OrderNo).IsUnique(); // 唯一索引 e.Property(p p.Amount) .HasPrecision(18, 2); // 金额精度 }); builder.EntityCustomer(e { e.ToTable(t_customer); e.Property(p p.Name).HasMaxLength(64); }); } }HasPrecision(18, 2)这行在 SQLServer 上会生成decimal(18,2)在 MySQL 上会生成decimal(18,2)两者语义一致。但如果你不写这行SQLServer 默认给decimal(18,2)MySQL 默认给decimal(65,30)同一个金额字段在两种库上精度完全不同这是后面会展开的一个坑。2.2 为什么用 Provider 抽象而不是写两套模型有人会问既然两种数据库有差异为什么不干脆写两套实体类答案是维护成本。业务逻辑依赖的是Order这个类型不是OrderForSqlServer。一旦分叉所有仓储、服务、DTO 映射都要跟着分叉改一个字段要动两处。正确做法是模型只有一套差异通过 Provider 包和少量条件配置吸收。EF Core 的 Provider 模型是这样的Microsoft.EntityFrameworkCore.SqlServer和Pomelo.EntityFrameworkCore.MySql各自实现同一套抽象接口DbContextOptionsBuilder.UseSqlServer()和.UseMySql()负责注入不同的 SQL 生成器和类型映射器。你的AppDbContext完全不知道底层连的是谁。// 根据配置切换 Provider var provider config[Database:Provider]; // SqlServer 或 MySql var connStr config.GetConnectionString(provider)!; services.AddDbContextAppDbContext(opt { if (provider SqlServer) opt.UseSqlServer(connStr); else opt.UseMySql(connStr, ServerVersion.AutoDetect(connStr)); });这段代码的关键点是ServerVersion.AutoDetect。MySQL 的 Provider 需要知道服务端版本才能决定生成什么语法比如 JSON 列、窗口函数自动探测会连一次数据库拿版本号。如果连不上启动就会报错这是新手最常见的第一个卡点。2.3 双数据库工程的最小目录结构一个能同时跑 SQLServer 和 MySQL 的工程目录不需要很复杂但职责要分清目录/文件职责是否随 Provider 变化Models/实体类否Data/AppDbContext.cs模型配置否Data/DesignTimeFactory.cs迁移时用的工厂是需读配置Migrations/SqlServer/SQLServer 迁移文件是Migrations/MySql/MySQL 迁移文件是appsettings.json连接串与 Provider 开关是迁移文件必须分目录存放因为两种数据库生成的 DDL 不同混在一起会互相覆盖。EF Core 支持通过MigrationsAssembly和--output-dir参数把迁移写到不同目录下一章会给出具体命令。3. 迁移落地为 SQLServer 和 MySQL 分别生成迁移文件模型定义好之后下一步是把它变成数据库里的表。EF Core 的迁移机制分两步dotnet ef migrations add根据模型快照生成迁移类dotnet ef database update把迁移应用到数据库。双数据库的关键在于这两步都要针对不同 Provider 各跑一次。3.1 安装工具与设计时工厂先装 CLI 工具和两个 Provider 包dotnet tool install --global dotnet-ef dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Pomelo.EntityFrameworkCore.MySqlMicrosoft.EntityFrameworkCore.Design是迁移命令必需的少了它dotnet ef会提示找不到设计时服务。Pomelo 是 MySQL 社区维护的 Provider对 EF Core 版本跟进比较及时是目前的主流选择。设计时工厂的作用是迁移命令执行时EF Core 需要一个能构造AppDbContext的入口但此时 ASP.NET Core 的依赖注入容器还没起来。工厂类负责手动读配置、构造 Options。// Data/DesignTimeFactory.cs public class DesignTimeFactory : IDesignTimeDbContextFactoryAppDbContext { public AppDbContext CreateDbContext(string[] args) { // 从环境变量读默认 SqlServer var provider Environment.GetEnvironmentVariable(EF_PROVIDER) ?? SqlServer; var builder new DbContextOptionsBuilderAppDbContext(); if (provider SqlServer) builder.UseSqlServer(Serverlocalhost;DatabaseDemoDb;Trusted_ConnectionTrue;TrustServerCertificateTrue); else builder.UseMySql(Serverlocalhost;DatabaseDemoDb;Userroot;Passwordyour_pwd, ServerVersion.AutoDetect(Serverlocalhost;DatabaseDemoDb;Userroot;Passwordyour_pwd)); return new AppDbContext(builder.Options); } }这里用环境变量EF_PROVIDER控制走哪个分支是为了让同一条迁移命令能通过切换环境变量生成不同 Provider 的迁移。连接串只是设计时用不要求真能连上除非用了AutoDetect它会真连。3.2 生成 SQLServer 迁移并应用# 生成 SQLServer 迁移输出到 Migrations/SqlServer $env:EF_PROVIDERSqlServer dotnet ef migrations add Init_SqlServer --output-dir Migrations/SqlServer # 应用到数据库 dotnet ef database update--output-dir把迁移文件写到指定目录避免和 MySQL 的混在一起。生成的迁移类里会包含migrationBuilder.CreateTable调用列类型是 SQLServer 方言比如nvarchar(32)、datetime2。3.3 生成 MySQL 迁移并应用# 切换 Provider生成 MySQL 迁移 $env:EF_PROVIDERMySql dotnet ef migrations add Init_MySql --output-dir Migrations/MySql # 应用 dotnet ef database updateMySQL 迁移里同样的字段会变成varchar(32)、datetime(6)。注意datetime(6)的 6 是微秒精度SQLServer 的datetime2默认 7 位精度这个差异在跨库比对数据时会造成毫秒级偏差后面避坑章节会细说。3.4 迁移文件里必须检查的三处差异生成完两份迁移后不要直接应用先打开文件对比三处第一主键自增语法。SQLServer 用Annotation(SqlServer:Identity, 1, 1)MySQL 用Annotation(MySql:ValueGenerationStrategy, MySqlValueGenerationStrategy.IdentityColumn)。如果哪份迁移里缺了自增注解插入数据时主键会是 0。第二字符串类型。SQLServer 默认nvarcharMySQL 默认varchar。如果业务里有中文MySQL 还要确认字符集是utf8mb4否则 emoji 和生僻字会丢。第三索引和唯一约束的命名。两种数据库对索引名长度限制不同MySQL 限制 64 字符SQLServer 限制 128 字符。如果模型里索引名很长MySQL 迁移可能直接报错。提示迁移文件生成后建议先dotnet ef migrations script导出 SQL 看一眼比直接 update 更安全尤其是生产环境。4. 双库差异排查类型映射、精度与连接串的五个坑同一套模型跑两种数据库翻车基本集中在类型映射和连接配置上。这一章按“现象 → 原因 → 解决”整理五条实际踩过的坑每条都给出可验证的判断方法。4.1 坑一decimal 精度不一致导致对账差几分钱现象同一笔订单SQLServer 里金额是100.00MySQL 里查出来是100.000000000000000000000000000000前端展示一长串。原因模型里没写HasPrecisionSQLServer 默认decimal(18,2)MySQL 默认decimal(65,30)。插入时值一样但存储精度不同聚合运算后尾差被放大。解决所有decimal属性强制配置精度。e.Property(p p.Amount).HasPrecision(18, 2);改完要重新生成两份迁移不能只改一份。判断方法dotnet ef migrations script导出后搜decimal两份都应该是(18,2)。4.2 坑二DateTime 精度差异导致排序错乱现象批量插入的几条记录CreatedAt看起来是同一秒但两种库查出来的顺序不一样。原因SQLServer 的datetime2默认 7 位小数秒100 纳秒MySQL 的datetime默认 0 位秒要显式写datetime(6)才有微秒。精度不同同一批数据落库后的实际值就不同。解决在 Fluent API 里统一指定精度或者业务层用 UTC 时间戳long存储。如果坚持用 DateTimeMySQL 侧要确认列类型是datetime(6)。e.Property(p p.CreatedAt).HasPrecision(6);4.3 坑三MySQL 连接串少了 AllowUserVariables 导致迁移失败现象dotnet ef database update在 MySQL 上报语法错误指向迁移里的某条 SQL。原因Pomelo 在某些版本下生成迁移 SQL 时会用到用户变量连接串没开AllowUserVariablesTrue就会被服务端拒绝。解决MySQL 连接串加上这个参数。Serverlocalhost;DatabaseDemoDb;Userroot;Passwordxxx;AllowUserVariablesTrue;CharSetutf8mb4CharSetutf8mb4同时解决中文和 emoji 问题。判断方法迁移报语法错误时先把连接串参数补齐再重试不要急着改迁移文件。4.4 坑四SQLServer 的 TrustServerCertificate 在容器里必开现象本地开发正常一放到容器里连 SQLServer 就报证书链错误。原因新版 SQLServer 驱动默认强制加密容器里的自签证书不被信任。解决开发环境连接串加TrustServerCertificateTrue生产环境用正规证书不要图省事一直开着。Serversqlserver;DatabaseDemoDb;Usersa;Passwordxxx;TrustServerCertificateTrue4.5 坑五迁移目录混用导致模型快照错乱现象先生成了 SQLServer 迁移再生成 MySQL 迁移时EF Core 提示“没有模型变更”生成出空迁移。原因EF Core 判断是否有变更靠的是对比当前模型和最近一次迁移的快照。如果两份迁移放在同一目录第二次生成时它读到的是 SQLServer 的快照认为模型没变。解决两份迁移必须分目录并且生成时用--output-dir明确指定。如果已经混了删掉 Migrations 目录重新按顺序生成。rm -rf Migrations $env:EF_PROVIDERSqlServer; dotnet ef migrations add Init_SqlServer --output-dir Migrations/SqlServer $env:EF_PROVIDERMySql; dotnet ef migrations add Init_MySql --output-dir Migrations/MySql注意删迁移目录前确认数据库里没有依赖这些迁移记录的数据生产环境不要直接删。5. 进阶技巧用同一套迁移脚本做双库验证与 CI 检查跑通双库之后真正省事的是把验证自动化。我一般会在 CI 里加一步对两种 Provider 分别导出 SQL 脚本做静态检查确保没有方言泄漏到不该出现的地方。5.1 导出双库 SQL 脚本做比对# 导出 SQLServer 脚本 $env:EF_PROVIDERSqlServer dotnet ef migrations script --idempotent -o artifacts/schema_sqlserver.sql # 导出 MySQL 脚本 $env:EF_PROVIDERMySql dotnet ef migrations script --idempotent -o artifacts/schema_mysql.sql--idempotent生成可重复执行的脚本适合放进发布流水线。导出后可以用脚本做两个检查一是搜decimal确认精度一致二是搜datetime确认精度声明存在。5.2 用测试库跑一遍真实读写光看 SQL 不够类型映射的坑往往在写入时才暴露。我会在 CI 里起两个临时库SQLServer 和 MySQL 各一个跑同一套集成测试。[Theory] [InlineData(SqlServer)] [InlineData(MySql)] public async Task Insert_And_Query_Order_Works(string provider) { await using var ctx TestDbFactory.Create(provider); await ctx.Database.EnsureCreatedAsync(); // 测试库用 EnsureCreated 更快 var order new Order { OrderNo T-001, Amount 100.00m, CreatedAt DateTime.UtcNow }; ctx.Orders.Add(order); await ctx.SaveChangesAsync(); var loaded await ctx.Orders.SingleAsync(o o.OrderNo T-001); Assert.Equal(100.00m, loaded.Amount); // 精度断言 Assert.Equal(order.CreatedAt, loaded.CreatedAt); // 时间精度断言 }这段测试的价值在于Assert.Equal对decimal和DateTime是精确比较任何精度差异都会让测试失败。EnsureCreatedAsync绕过迁移直接按模型建表适合测试环境生产环境仍然用迁移。5.3 参数配置速查表配置项SQLServer 写法MySQL 写法说明Provider 包Microsoft.EntityFrameworkCore.SqlServerPomelo.EntityFrameworkCore.MySql二选一注入连接串关键参数TrustServerCertificateTrueAllowUserVariablesTrue;CharSetutf8mb4开发环境自增注解SqlServer:IdentityMySql:ValueGenerationStrategy迁移文件里检查字符串类型nvarcharvarchar中文场景确认 utf8mb4时间精度datetime2(7)datetime(6)统一用 HasPrecision(6)迁移目录Migrations/SqlServerMigrations/MySql必须分开5.4 一个我坚持了很久的习惯每次改完模型我不会直接database update而是先migrations script导出两份 SQL肉眼扫一遍decimal、datetime、varchar这三类字段的声明。这个习惯帮我拦下过至少三次精度不一致的问题——它们不会在开发环境报错只会在生产对账时冒出来那时候再改就要写数据修复脚本了。双库项目里模型是唯一事实来源这句话只在迁移脚本被认真检查过之后才成立。希望帮到你。本文还有配套的精品资源点击获取