EF Core Code First 实战:从建模到迁移管理
2018.02.14
C#
3 min
1.3k 字
// 目录 · contents
1. 实体建模 2. Fluent API 配置 3. 迁移管理 4. 生产环境迁移策略 5. 值对象(Value Object) 踩坑记录 总结
从 EF 6 迁移到 EF Core 之后,API 有不少变化,但整体思路是一致的。EF
Core 的 Code First 工作流在团队协作和版本管理上比直接写 SQL
更顺畅,这篇文章总结了我在实际项目中的使用经验。
1. 实体建模
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Order { public int Id { get ; set ; } public string OrderNo { get ; set ; } public DateTime CreatedAt { get ; set ; } public OrderStatus Status { get ; set ; } public decimal TotalAmount { get ; set ; } public int UserId { get ; set ; } public User User { get ; set ; } public List<OrderItem> Items { get ; set ; } = new List<OrderItem>(); }public class OrderItem { public int Id { get ; set ; } public int OrderId { get ; set ; } public Order Order { get ; set ; } public int ProductId { get ; set ; } public Product Product { get ; set ; } public int Quantity { get ; set ; } public decimal UnitPrice { get ; set ; } }
2. Fluent API 配置
Data Annotation(特性)适合简单场景,Fluent API
更强大且不污染实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class AppDbContext : DbContext { public DbSet<Order> Orders { get ; set ; } public DbSet<OrderItem> OrderItems { get ; set ; } public DbSet<User> Users { get ; set ; } protected override void OnModelCreating (ModelBuilder modelBuilder ) { modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } }public class OrderConfiguration : IEntityTypeConfiguration <Order > { public void Configure (EntityTypeBuilder<Order> builder ) { builder.ToTable("orders" ); builder.HasKey(o => o.Id); builder.Property(o => o.OrderNo) .IsRequired() .HasMaxLength(32 ); builder.Property(o => o.TotalAmount) .HasColumnType("decimal(18, 2)" ); builder.Property(o => o.CreatedAt) .HasDefaultValueSql("GETUTCDATE()" ); builder.HasIndex(o => o.OrderNo).IsUnique(); builder.HasIndex(o => new { o.UserId, o.Status, o.CreatedAt }); builder.HasOne(o => o.User) .WithMany(u => u.Orders) .HasForeignKey(o => o.UserId) .OnDelete(DeleteBehavior.Restrict); builder.HasMany(o => o.Items) .WithOne(i => i.Order) .HasForeignKey(i => i.OrderId); builder.HasQueryFilter(o => !o.IsDeleted); builder.Property(o => o.Status) .HasConversion<string >(); } }
3. 迁移管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 dotnet ef migrations add InitialCreate dotnet ef migrations script dotnet ef database update dotnet ef database update PreviousMigrationName dotnet ef migrations remove
生成的迁移文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public partial class AddOrderStatusIndex : Migration { protected override void Up (MigrationBuilder migrationBuilder ) { migrationBuilder.CreateIndex( name: "IX_orders_UserId_Status_CreatedAt" , table: "orders" , columns: new [] { "UserId" , "Status" , "CreatedAt" }); } protected override void Down (MigrationBuilder migrationBuilder ) { migrationBuilder.DropIndex( name: "IX_orders_UserId_Status_CreatedAt" , table: "orders" ); } }
4. 生产环境迁移策略
不推荐 :在应用启动时自动迁移(db.Database.Migrate())——如果有多个实例同时启动,并发迁移可能导致问题。
推荐 :生成 SQL 脚本,通过 CI/CD
流水线在部署前应用:
1 2 dotnet ef migrations script --idempotent -o migration.sql
--idempotent
参数生成的脚本包含版本检查,可以安全地重复执行。
1 2 3 4 5 6 7 IF NOT EXISTS (SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'20180214_AddOrderStatusIndex' )BEGIN CREATE INDEX [IX_orders_UserId_Status_CreatedAt] ON [orders] ([UserId], [Status], [CreatedAt]); INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'20180214_AddOrderStatusIndex' , N'2.0.1' );END ;
5. 值对象(Value Object)
DDD 中的值对象(没有独立 ID)在 EF Core 2.0+ 可以用 Owned Entity
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Order { public Address ShippingAddress { get ; set ; } }public class Address { public string Province { get ; set ; } public string City { get ; set ; } public string Street { get ; set ; } public string ZipCode { get ; set ; } } builder.OwnsOne(o => o.ShippingAddress, address => { address.Property(a => a.Province).HasColumnName("shipping_province" ); address.Property(a => a.City).HasColumnName("shipping_city" ); address.Property(a => a.Street).HasColumnName("shipping_street" ); address.Property(a => a.ZipCode).HasColumnName("shipping_zipcode" ); });
Address 的字段会被内联到 orders
表中,不会创建独立的表。
踩坑记录
我们有个迁移在本地和测试环境都没问题,在生产环境应用时出错了。原因是生产环境数据库有一张历史遗留表
user_profiles,而这次迁移恰好要创建同名的表(我们重构了模型)。
EF Core
的迁移只知道它自己管理的历史(__EFMigrationsHistory
表),不知道数据库里已有的对象。
解决方案:迁移前先手动备份并 rename 旧表,或者在迁移文件里加
migrationBuilder.Sql("EXEC sp_rename...")
手动处理冲突。
从那以后,我们在 CI
里加了一步:在一个干净的数据库快照上跑迁移,确认没问题后再上生产。
总结
EF Core Code First 最佳实践:
Fluent API 放在独立的 Configuration
类里 ,实体类保持干净
迁移文件提交到版本控制 ,团队成员 pull
后能重建数据库
生产环境用幂等 SQL
脚本 ,而不是程序启动时自动迁移
HasQueryFilter
实现软删除 ,全局生效,无需每次手写 WHERE
HasConversion 存储枚举为字符串 ,比存
int 更具可读性