Skip to content

表拆分(Table Splitting) ​

概述 ​

表拆分(Table Splitting)是 EF Core 的一种高级映射技术,允许多个实体类型共享同一个数据库表。这与传统的"一表一实体"模式不同,表拆分可以将一个大表逻辑上拆分为多个实体,或者将相关数据组织到不同的实体类中,但物理上存储在同一个表中。

使用场景 ​

  • 大表优化: 将常用字段和不常用字段分离
  • 性能优化: 减少查询时加载的数据量
  • 关注点分离: 将不同业务逻辑的数据分开
  • 遗留系统: 适配现有数据库结构

基础概念 ​

传统模式 vs 表拆分 ​

传统模式(一表一实体):

数据库表: Products
┌────┬──────────┬───────┬────────────┬──────────────┐
│ Id │ Name     │ Price │ Stock      │ Description  │
└────┴──────────┴───────┴────────────┴──────────────┘

C# 实体: Product (包含所有字段)

表拆分模式:

数据库表: Products (同一个表)
┌────┬──────────┬───────┬────────────┬──────────────┐
│ Id │ Name     │ Price │ Stock      │ Description  │
└────┴──────────┴───────┴────────────┴──────────────┘

C# 实体: 
- ProductInfo (Id, Name, Price) - 常用信息
- ProductInventory (Id, Stock) - 库存信息
- ProductDetails (Id, Description) - 详细信息

配置方法 ​

示例 1: 基本信息与详细信息拆分 ​

定义实体 ​

csharp
// 产品基本信息
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    
    // 导航属性指向详细信息
    public ProductDetails? Details { get; set; }
}

// 产品详细信息
public class ProductDetails
{
    public int Id { get; set; }
    public string Description { get; set; } = string.Empty;
    public string Specifications { get; set; } = string.Empty;
    public byte[]? ImageData { get; set; }
    
    // 导航属性指向基本信息
    public Product? Product { get; set; }
}

Fluent API 配置 ​

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 配置 Product
    modelBuilder.Entity<Product>(entity =>
    {
        entity.ToTable("Products");
        entity.HasKey(p => p.Id);
        
        entity.Property(p => p.Name).IsRequired().HasMaxLength(200);
        entity.Property(p => p.Price).HasColumnType("decimal(18,2)");
    });
    
    // 配置 ProductDetails - 共享同一个表
    modelBuilder.Entity<ProductDetails>(entity =>
    {
        entity.ToTable("Products"); // 关键: 指定相同的表名
        
        entity.HasKey(d => d.Id);
        
        // 配置一对一关系
        entity.HasOne(d => d.Product)
              .WithOne(p => p.Details)
              .HasForeignKey<ProductDetails>(d => d.Id); // 共享主键
        
        entity.Property(d => d.Description).HasMaxLength(2000);
        entity.Property(d => d.Specifications).HasMaxLength(4000);
        entity.Property(d => d.ImageData).HasColumnType("varbinary(max)");
    });
}

使用示例 ​

csharp
// 只查询基本信息(不加载详细信息)
var products = await context.Products
    .Where(p => p.Price > 100)
    .ToListAsync();

// 需要时才加载详细信息
var productWithDetails = await context.Products
    .Include(p => p.Details)
    .FirstOrDefaultAsync(p => p.Id == 1);

Console.WriteLine(productWithDetails.Name);
Console.WriteLine(productWithDetails.Details?.Description);

示例 2: 用户信息与认证信息拆分 ​

定义实体 ​

csharp
// 用户基本信息
public class User
{
    public int Id { get; set; }
    public string Username { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    
    public UserAuthentication? Authentication { get; set; }
}

// 用户认证信息(敏感数据)
public class UserAuthentication
{
    public int Id { get; set; }
    public string PasswordHash { get; set; } = string.Empty;
    public string Salt { get; set; } = string.Empty;
    public DateTime? LastLoginAt { get; set; }
    public int FailedLoginAttempts { get; set; }
    
    public User? User { get; set; }
}

配置 ​

csharp
modelBuilder.Entity<User>(entity =>
{
    entity.ToTable("Users");
    entity.HasKey(u => u.Id);
});

modelBuilder.Entity<UserAuthentication>(entity =>
{
    entity.ToTable("Users"); // 共享 Users 表
    
    entity.HasKey(a => a.Id);
    
    entity.HasOne(a => a.User)
          .WithOne(u => u.Authentication)
          .HasForeignKey<UserAuthentication>(a => a.Id);
    
    // 敏感字段
    entity.Property(a => a.PasswordHash).IsRequired();
    entity.Property(a => a.Salt).IsRequired();
});

使用场景 ​

csharp
// 场景 1: 显示用户列表(不需要密码信息)
var users = await context.Users
    .Select(u => new { u.Id, u.Username, u.Email })
    .ToListAsync();

// 场景 2: 用户登录(需要认证信息)
var userWithAuth = await context.Users
    .Include(u => u.Authentication)
    .FirstOrDefaultAsync(u => u.Username == username);

if (userWithAuth != null && VerifyPassword(password, userWithAuth.Authentication.PasswordHash))
{
    // 登录成功
    userWithAuth.Authentication.LastLoginAt = DateTime.UtcNow;
    userWithAuth.Authentication.FailedLoginAttempts = 0;
    await context.SaveChangesAsync();
}
else
{
    // 记录失败尝试
    if (userWithAuth != null)
    {
        userWithAuth.Authentication.FailedLoginAttempts++;
        await context.SaveChangesAsync();
    }
}

示例 3: 订单统计信息拆分 ​

定义实体 ​

csharp
// 订单基本信息
public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public DateTime OrderDate { get; set; }
    public string Status { get; set; } = "Pending";
    
    public OrderStatistics? Statistics { get; set; }
}

// 订单统计信息(计算字段)
public class OrderStatistics
{
    public int Id { get; set; }
    public int ItemCount { get; set; }
    public decimal Subtotal { get; set; }
    public decimal TaxAmount { get; set; }
    public decimal ShippingCost { get; set; }
    public decimal TotalAmount { get; set; }
    
    public Order? Order { get; set; }
}

配置 ​

csharp
modelBuilder.Entity<Order>(entity =>
{
    entity.ToTable("Orders");
    entity.HasKey(o => o.Id);
});

modelBuilder.Entity<OrderStatistics>(entity =>
{
    entity.ToTable("Orders"); // 共享 Orders 表
    
    entity.HasKey(s => s.Id);
    
    entity.HasOne(s => s.Order)
          .WithOne(o => o.Statistics)
          .HasForeignKey<OrderStatistics>(s => s.Id);
});

高级配置 ​

1. 可选的拆分实体 ​

csharp
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    // 可选: 可能没有详细信息
    public ProductDetails? Details { get; set; }
}

modelBuilder.Entity<ProductDetails>(entity =>
{
    entity.ToTable("Products");
    
    entity.HasOne(d => d.Product)
          .WithOne(p => p.Details)
          .HasForeignKey<ProductDetails>(d => d.Id)
          .IsRequired(false); // 可选关系
});

2. 多个拆分实体 ​

csharp
// 一个主实体,多个拆分实体
public class Article
{
    public int Id { get; set; }
    public string Title { get; set; }
    public DateTime PublishedAt { get; set; }
    
    public ArticleContent? Content { get; set; }
    public ArticleMetadata? Metadata { get; set; }
    public ArticleStatistics? Statistics { get; set; }
}

// 配置所有实体共享 Articles 表
modelBuilder.Entity<Article>().ToTable("Articles");
modelBuilder.Entity<ArticleContent>().ToTable("Articles");
modelBuilder.Entity<ArticleMetadata>().ToTable("Articles");
modelBuilder.Entity<ArticleStatistics>().ToTable("Articles");

// 配置外键关系
modelBuilder.Entity<ArticleContent>()
    .HasOne(c => c.Article)
    .WithOne(a => a.Content)
    .HasForeignKey<ArticleContent>(c => c.Id);

modelBuilder.Entity<ArticleMetadata>()
    .HasOne(m => m.Article)
    .WithOne(a => a.Metadata)
    .HasForeignKey<ArticleMetadata>(m => m.Id);

modelBuilder.Entity<ArticleStatistics>()
    .HasOne(s => s.Article)
    .WithOne(a => a.Statistics)
    .HasForeignKey<ArticleStatistics>(s => s.Id);

3. 并发控制 ​

csharp
modelBuilder.Entity<Product>(entity =>
{
    entity.ToTable("Products");
    entity.Property(p => p.RowVersion).IsRowVersion();
});

modelBuilder.Entity<ProductDetails>(entity =>
{
    entity.ToTable("Products");
    // 共享主实体的 RowVersion
});

性能优化 ​

1. 减少数据传输 ​

csharp
// ❌ 不使用表拆分: 每次都加载大字段
var products = await context.Products.ToListAsync();
// SELECT Id, Name, Price, Description, Specifications, ImageData FROM Products

// ✅ 使用表拆分: 只加载需要的数据
var products = await context.Products
    .Select(p => new { p.Id, p.Name, p.Price })
    .ToListAsync();
// SELECT Id, Name, Price FROM Products

// 需要时才加载详细信息
var product = await context.Products
    .Include(p => p.Details)
    .FirstOrDefaultAsync(p => p.Id == 1);

2. 投影查询优化 ​

csharp
// 只选择需要的拆分实体
var productSummaries = await context.Products
    .Select(p => new ProductSummaryDto
    { 
        Id = p.Id,
        Name = p.Name,
        Price = p.Price,
        HasDetails = p.Details != null
    })
    .ToListAsync();

3. 批量操作 ​

csharp
// 更新基本信息
await context.Products
    .Where(p => p.CategoryId == 1)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.Price, p => p.Price * 1.1m));

// 更新详细信息
await context.Set<ProductDetails>()
    .Where(d => d.Product!.CategoryId == 1)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(d => d.Specifications, "Updated"));

最佳实践 ​

✅ 推荐做法 ​

1. 根据访问频率拆分 ​

csharp
// 高频访问字段 -> 主实体
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }      // 经常查询
    public decimal Price { get; set; }    // 经常查询
}

// 低频访问字段 -> 拆分实体
public class ProductDetails
{
    public int Id { get; set; }
    public string Description { get; set; }   // 偶尔查看
    public byte[] ImageData { get; set; }     // 很少访问,大数据
}

2. 根据安全性要求拆分 ​

csharp
// 公开信息
public class User
{
    public string Username { get; set; }
    public string Email { get; set; }
}

// 敏感信息
public class UserCredentials
{
    public string PasswordHash { get; set; }
    public string ApiKey { get; set; }
}

3. 添加适当的索引 ​

sql
-- 为主键创建聚簇索引
CREATE CLUSTERED INDEX IX_Products_Id ON Products(Id);

-- 为常用查询字段创建非聚簇索引
CREATE NONCLUSTERED INDEX IX_Products_Price ON Products(Price);

❌ 避免的陷阱 ​

1. 不要过度拆分 ​

csharp
// ❌ 错误: 拆分太细,增加复杂度
public class ProductBasic { }
public class ProductPrice { }
public class ProductStock { }
public class ProductDescription { }

// ✅ 正确: 合理分组
public class Product { }         // 基本信息
public class ProductDetails { }  // 详细信息

建议: 每个表最多 2-3 个拆分实体


2. 不要忘记配置关系 ​

csharp
// ❌ 错误: 忘记配置外键
modelBuilder.Entity<ProductDetails>(entity =>
{
    entity.ToTable("Products");
    // 忘记配置 HasOne/WithOne/HasForeignKey
});

// ✅ 正确: 完整配置
modelBuilder.Entity<ProductDetails>(entity =>
{
    entity.ToTable("Products");
    entity.HasOne(d => d.Product)
          .WithOne(p => p.Details)
          .HasForeignKey<ProductDetails>(d => d.Id);
});

3. 注意空值处理 ​

csharp
// ❌ 可能导致 NullReferenceException
var description = product.Details.Description;

// ✅ 安全访问
var description = product.Details?.Description ?? "N/A";

// 或使用条件加载
var product = await context.Products
    .Include(p => p.Details)
    .FirstOrDefaultAsync(p => p.Id == id);

与其他技术对比 ​

技术表数量实体数量适用场景
表拆分1个表多个实体大表优化,关注点分离
表分片多个表1个实体水平扩展,分库分表
TPH继承1个表多个实体类型层次结构
TPT继承多个表多个实体严格的类型隔离

总结 ​

核心优势 ​

✅ 性能提升: 减少不必要的数据加载
✅ 灵活性强: 按需加载不同部分
✅ 安全性好: 敏感数据独立管理
✅ 维护性高: 关注点清晰分离

适用场景 ​

  • 大文本字段(Description, Content)
  • 二进制数据(Image, File)
  • 敏感信息(Password, Token)
  • 统计数据(Count, Sum)
  • 审计日志(CreatedAt, ModifiedAt)

注意事项 ​

⚠️ 不要过度拆分(保持简单)
⚠️ 始终配置正确的关系
⚠️ 注意空值和异常处理
⚠️ 测试查询性能

表拆分是优化 EF Core 性能的有力工具,合理使用可以显著提升应用效率! 🚀

基于 MIT 许可发布