Skip to content

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 FirstDatabase 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 update

2. 实体设计与配置 ​

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.sql

3.3 迁移管理 ​

bash
# 查看待应用的迁移
dotnet ef migrations list

# 删除最后一个迁移(未应用)
dotnet ef migrations remove

# 查看迁移详情
dotnet ef migrations script PreviousMigration NextMigration

4. 数据库初始化策略 ​

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 SeedInitialData

5.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 最佳实践清单 ​

✅ 应该做的:

  1. 始终使用迁移

    bash
    # ✅ 好
    dotnet ef migrations add ChangeName
    dotnet ef database update
    
    # ❌ 坏: 手动修改数据库
  2. 版本控制迁移文件

    bash
    git add Migrations/
    git commit -m "Add product category migration"
  3. 小步迭代

    bash
    # ✅ 好: 多个小迁移
    dotnet ef migrations add AddCategoryTable
    dotnet ef migrations add AddCategoryForeignKey
    
    # ❌ 坏: 一个大迁移包含所有变更
  4. 审查生成的迁移

    csharp
    // 检查Up和Down方法是否正确
    // 确保不会丢失数据

❌ 不应该做的:

  1. 不要手动编辑迁移文件(除非必要)
  2. 不要在生产环境直接Update
  3. 不要忘记测试Down方法
  4. 不要混用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友好

关键工作流 ​

  1. 设计实体 - 定义领域模型
  2. 配置映射 - Fluent API
  3. 创建迁移 - dotnet ef migrations add
  4. 应用迁移 - dotnet ef database update
  5. 迭代演进 - 持续重构

掌握Code First,你可以高效地开发和维护数据访问层!

基于 MIT 许可发布