Appearance
Code First(代码优先)工作流
Code First是EF Core最流行的开发模式,允许开发者通过C#类定义数据模型,然后由EF Core自动创建和维护数据库架构。本章将深入探讨Code First的完整工作流程,包括实体设计、迁移管理、数据库初始化以及最佳实践。
目录
1. Code First基础
1.1 什么是Code First?
核心概念:
csharp
// Code First开发流程:
// 1. 编写C#实体类
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
// 2. 定义DbContext
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
}
// 3. 创建迁移
// dotnet ef migrations add InitialCreate
// 4. 应用迁移(自动生成数据库)
// dotnet ef database update
// 结果: EF Core自动创建Products表Code First vs Database First对比:
| 特性 | Code First | Database First |
|---|---|---|
| 起点 | C#代码 | 现有数据库 |
| 控制权 | 代码控制数据库 | 数据库控制代码 |
| 适用场景 | 新项目 | 遗留系统 |
| 灵活性 | 高 | 中 |
| 学习曲线 | 低 | 中 |
| 团队协作 | 容易(Git友好) | 困难 |
1.2 快速开始
步骤1: 安装NuGet包
bash
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools步骤2: 定义实体
csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public decimal Price { get; set; }
public int Stock { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}步骤3: 创建DbContext
csharp
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置实体
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(p => p.Id);
entity.Property(p => p.Name).IsRequired().HasMaxLength(200);
entity.Property(p => p.Price).HasColumnType("decimal(18,2)");
entity.HasIndex(p => p.Name);
});
}
}步骤4: 注册服务
csharp
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));步骤5: 创建并应用迁移
bash
# 创建迁移
dotnet ef migrations add InitialCreate
# 应用迁移(创建数据库)
dotnet ef database update2. 实体设计与配置
2.1 约定优于配置
EF Core默认约定:
csharp
public class Product
{
// ✅ 约定: 名为Id或{ClassName}Id的属性自动成为主键
public int Id { get; set; }
// ✅ 约定: 字符串映射为nvarchar(max)
public string Name { get; set; }
// ✅ 约定: decimal映射为decimal(18,2)
public decimal Price { get; set; }
// ✅ 约定: 可空类型允许NULL
public DateTime? DiscontinuedAt { get; set; }
// ✅ 约定: 集合属性自动成为导航属性
public List<OrderItem> OrderItems { get; set; }
}2.2 Fluent API配置
完整配置示例:
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Product配置
modelBuilder.Entity<Product>(entity =>
{
// 主键
entity.HasKey(p => p.Id);
// 表名
entity.ToTable("Products", "dbo");
// 属性配置
entity.Property(p => p.Name)
.IsRequired()
.HasMaxLength(200)
.HasColumnName("ProductName");
entity.Property(p => p.Price)
.HasColumnType("decimal(18,2)")
.HasDefaultValue(0m);
entity.Property(p => p.Stock)
.HasDefaultValue(0)
.ValueGeneratedOnAdd();
// 索引
entity.HasIndex(p => p.Name)
.IsUnique();
entity.HasIndex(p => p.Price);
// 关系配置
entity.HasMany(p => p.OrderItems)
.WithOne(oi => oi.Product)
.HasForeignKey(oi => oi.ProductId)
.OnDelete(DeleteBehavior.Restrict);
});
}2.3 复杂关系配置
一对多关系:
csharp
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
// 导航属性
public List<Product> Products { get; set; } = new();
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
// 外键
public int CategoryId { get; set; }
// 导航属性
public Category? Category { get; set; }
}
// 配置
modelBuilder.Entity<Category>()
.HasMany(c => c.Products)
.WithOne(p => p.Category)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Cascade);多对多关系(.NET 5+):
csharp
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public List<Course> Courses { get; set; } = new();
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
public List<Student> Students { get; set; } = new();
}
// 配置(自动创建连接表)
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity(j => j.ToTable("StudentCourses"));3. 迁移工作流
3.1 创建迁移
bash
# 基本命令
dotnet ef migrations add AddProductCategory
# 指定输出目录
dotnet ef migrations add AddProductCategory --output-dir Migrations/Products
# 添加注释
dotnet ef migrations add AddProductCategory --message "添加产品分类支持"生成的迁移文件:
csharp
[Migration("20240115103000_AddProductCategory")]
public partial class AddProductCategory : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// 添加分类表
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(maxLength: 100, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.Id);
});
// 为产品表添加外键
migrationBuilder.AddColumn<int>(
name: "CategoryId",
table: "Products",
nullable: false,
defaultValue: 1);
migrationBuilder.CreateIndex(
name: "IX_Products_CategoryId",
table: "Products",
column: "CategoryId");
migrationBuilder.AddForeignKey(
name: "FK_Products_Categories_CategoryId",
table: "Products",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Products_Categories_CategoryId",
table: "Products");
migrationBuilder.DropIndex(
name: "IX_Products_CategoryId",
table: "Products");
migrationBuilder.DropColumn(
name: "CategoryId",
table: "Products");
migrationBuilder.DropTable(
name: "Categories");
}
}3.2 应用迁移
bash
# 应用到最新迁移
dotnet ef database update
# 应用到特定迁移
dotnet ef database update AddProductCategory
# 回滚所有迁移
dotnet ef database update 0
# 生成SQL脚本(生产环境推荐)
dotnet ef migrations script -o migration.sql3.3 迁移管理
bash
# 查看待应用的迁移
dotnet ef migrations list
# 删除最后一个迁移(未应用)
dotnet ef migrations remove
# 查看迁移详情
dotnet ef migrations script PreviousMigration NextMigration4. 数据库初始化策略
4.1 EnsureCreated(仅开发)
csharp
// Program.cs - 仅用于开发/测试
using var scope = app.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 如果数据库不存在则创建(不使用迁移)
if (context.Database.EnsureCreated())
{
Console.WriteLine("Database created successfully");
}
else
{
Console.WriteLine("Database already exists");
}
// ⚠️ 警告: EnsureCreated不与迁移配合使用!4.2 Migrate(推荐)
csharp
// Program.cs - 生产环境推荐
using var scope = app.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 应用所有待处理的迁移
await context.Database.MigrateAsync();
Console.WriteLine("All migrations applied");4.3 条件初始化
csharp
public static class DatabaseInitializer
{
public static async Task InitializeAsync(AppDbContext context)
{
// 检查是否有待处理的迁移
var pendingMigrations = await context.Database.GetPendingMigrationsAsync();
if (pendingMigrations.Any())
{
Console.WriteLine($"Applying {pendingMigrations.Count()} migrations...");
await context.Database.MigrateAsync();
}
else
{
Console.WriteLine("Database is up to date");
}
// 可选: 种子数据
await SeedDataAsync(context);
}
private static async Task SeedDataAsync(AppDbContext context)
{
if (!context.Categories.Any())
{
context.Categories.AddRange(
new Category { Name = "Electronics" },
new Category { Name = "Books" }
);
await context.SaveChangesAsync();
}
}
}
// 使用
await DatabaseInitializer.InitializeAsync(context);5. 种子数据
5.1 HasData(迁移中)
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>().HasData(
new Category { Id = 1, Name = "Electronics" },
new Category { Id = 2, Name = "Books" },
new Category { Id = 3, Name = "Clothing" }
);
modelBuilder.Entity<Product>().HasData(
new Product
{
Id = 1,
Name = "Laptop",
Price = 999.99m,
CategoryId = 1
},
new Product
{
Id = 2,
Name = "C# Book",
Price = 49.99m,
CategoryId = 2
}
);
}
// 创建迁移时会包含种子数据
// dotnet ef migrations add SeedInitialData5.2 运行时种子数据
csharp
public static class DataSeeder
{
public static async Task SeedAsync(AppDbContext context)
{
// 检查是否已有数据
if (await context.Categories.AnyAsync())
return;
// 种子数据
var categories = new List<Category>
{
new() { Name = "Electronics" },
new() { Name = "Books" }
};
await context.Categories.AddRangeAsync(categories);
await context.SaveChangesAsync();
var products = new List<Product>
{
new()
{
Name = "Laptop",
Price = 999.99m,
CategoryId = categories[0].Id
},
new()
{
Name = "Phone",
Price = 699.99m,
CategoryId = categories[0].Id
}
};
await context.Products.AddRangeAsync(products);
await context.SaveChangesAsync();
}
}
// 在Program.cs中调用
await DataSeeder.SeedAsync(context);6. 重构与演进
6.1 重命名属性
csharp
// 原实体
public class Product
{
public int Id { get; set; }
public string ProductName { get; set; } // 想改为Name
}
// 修改实体
public class Product
{
public int Id { get; set; }
public string Name { get; set; } // 重命名
}
// 手动编辑迁移文件
public partial class RenameProductNameToName : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "ProductName",
table: "Products",
newName: "Name");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Name",
table: "Products",
newName: "ProductName");
}
}6.2 拆分实体
csharp
// 原实体
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Street { get; set; } // 要拆分
public string City { get; set; } // 要拆分
public string ZipCode { get; set; } // 要拆分
}
// 重构后
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public Address Address { get; set; } // 值对象
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
// 配置
modelBuilder.Entity<Customer>(entity =>
{
entity.OwnsOne(c => c.Address, address =>
{
address.Property(a => a.Street).HasColumnName("Street");
address.Property(a => a.City).HasColumnName("City");
address.Property(a => a.ZipCode).HasColumnName("ZipCode");
});
});7. 最佳实践与常见陷阱
7.1 最佳实践清单
✅ 应该做的:
始终使用迁移
bash# ✅ 好 dotnet ef migrations add ChangeName dotnet ef database update # ❌ 坏: 手动修改数据库版本控制迁移文件
bashgit add Migrations/ git commit -m "Add product category migration"小步迭代
bash# ✅ 好: 多个小迁移 dotnet ef migrations add AddCategoryTable dotnet ef migrations add AddCategoryForeignKey # ❌ 坏: 一个大迁移包含所有变更审查生成的迁移
csharp// 检查Up和Down方法是否正确 // 确保不会丢失数据
❌ 不应该做的:
- 不要手动编辑迁移文件(除非必要)
- 不要在生产环境直接Update
- 不要忘记测试Down方法
- 不要混用EnsureCreated和Migrate
7.2 常见问题解决
问题1: 迁移冲突
bash
# 两个开发者同时创建迁移
git pull # 拉取对方的迁移
dotnet ef migrations remove # 删除自己的
dotnet ef migrations add UpdatedMigration # 重新创建问题2: 迁移失败
bash
# 查看详细错误
dotnet ef database update --verbose
# 回滚到上一个稳定版本
dotnet ef database update PreviousMigration问题3: 数据丢失警告
csharp
// 迁移会删除列时
// 手动编辑迁移,先备份数据
migrationBuilder.Sql(@"
CREATE TABLE Products_Backup AS SELECT * FROM Products
");
// 执行变更
migrationBuilder.DropColumn(...);
// 恢复数据
migrationBuilder.Sql(@"
INSERT INTO Products (Id, Name)
SELECT Id, Name FROM Products_Backup
");总结
Code First是EF Core推荐的开发模式:
核心优势
✅ 代码驱动 - 完全控制模型
✅ 易于重构 - 迁移支持演进
✅ 团队协作 - Git友好
关键工作流
- 设计实体 - 定义领域模型
- 配置映射 - Fluent API
- 创建迁移 -
dotnet ef migrations add - 应用迁移 -
dotnet ef database update - 迭代演进 - 持续重构
掌握Code First,你可以高效地开发和维护数据访问层!