把一个实体类型映射到多个表,官方叫法是 Entity splitting,这个称呼有点难搞,要是翻译为"实体拆分"或"拆分实体",你第一感觉会不会认为是把一个表拆分为多个实体的意思。可它的含义是正好相反。为了避免大伙伴们产生误解,老周直接叫它"一个实体映射到多个表",虽然不言简,但很意赅。
把一个实体类对应到数据库中的多个表,本质上是啥呢?一对一,是不是?举个例子,看图。

恭喜你猜对了,正如上图所示,假设老周收了几个徒弟,上述三个表其实都是【学生】实体类拆开的。第一个表是学生的基础信息,第二个表是补充信息,第三个表是学生的联系方式。第二、三个表中的行必须与第一个表中的行一一对应。
基于这样的理解,咱们可以得出:第一个表有主键A,第二个表有个外键FA引用主键A,第三个表有个外键FB引用主键A。同时,考虑到第二、三个表中的数据是完全依赖第一个表的,所以,第二、三个表中可以把主键和外键设定为同一个列。说人话就是有一列既做当前表的主键,也做外键引用第一个表。这使得第二、三个表中每一条记录的主键列的值必须与第一个表中的主键列相同。

下面咱们举个例子说明一下。假设有这样一个实体。
/// <summary>
/// 宠物
/// </summary>
public class Pet
{
/// <summary>
/// 主键
/// </summary>
public int PetId { get; set; }
/// <summary>
/// 昵称
/// </summary>
public string NickName { get; set; } = "天外物种";
/// <summary>
/// 体重
/// </summary>
public float? Weight { get; set; }
/// <summary>
/// 体长
/// </summary>
public int? Length { get; set; }
/// <summary>
/// 毛色
/// </summary>
public string? Color { get; set; }
/// <summary>
/// 分类
/// </summary>
public string? Category { get; set; }
/// <summary>
/// 爱好
/// </summary>
public string[] Hobbies { get; set; } = [];
/// <summary>
/// 性格
/// </summary>
public string? Temperament { get; set; }
}
于是我有个想法,把这个实体映射到一个表中好像太长,拆开为三个表多好。
1、基本信息。ID,名称,宠物类别;
2、基础特征。毛色,体长体重等;
3、额外信息。爱好,性格等。
一、错误用法
脑细胞活跃的大伙伴们可能想到了怎么做了,于是:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Pet>(entity =>
{
// 文本类型的配置一下长度,不然全是 MAX 也不划算
entity.Property(d => d.NickName).HasMaxLength(20);
entity.Property(d => d.Color).HasMaxLength(12);
entity.Property(d => d.Category).HasMaxLength(15);
entity.Property(d => d.Hobbies).HasMaxLength(100);
entity.Property(d => d.Temperament).HasMaxLength(30);
// 给主键命个名
entity.HasKey(d => d.PetId).HasName("PK_my_pet");
entity.ToTable("tb_pet", tb =>
{
tb.Property(x => x.PetId).HasColumnName("pet_id");
tb.Property(x => x.NickName).HasColumnName("name");
tb.Property(x => x.Category).HasColumnName("cate");
});
entity.ToTable("tb_pet_chars", tb =>
{
tb.Property(p => p.PetId).HasColumnName("_pid");
tb.Property(p => p.Weight).HasColumnName("weight");
tb.Property(p => p.Length).HasColumnName("len");
tb.Property(p => p.Color).HasColumnName("fur_color");
});
entity.ToTable("tb_pet_other", tb =>
{
tb.Property(x => x.PetId).HasColumnName("_pid");
tb.Property(x => x.Temperament).HasColumnName("tempera");
tb.Property(x => x.Hobbies).HasColumnName("hobbies");
});
// 配置外键
entity.HasOne<Pet>()
.WithOne()
.HasForeignKey<Pet>(p => p.PetId)
.HasConstraintName("FK_petid");
});
}
映射了三个表,最后创建一个外键,指向主键------自己引用自己。代码看着挺合理,但运行会报错。

错误是在模型验证过程中发生的,即验证失败。该异常是在 RelationalModelValidator 类的 ValidatePropertyOverrides 方法中抛出的,咱们进去看看源代码。
protected virtual void ValidatePropertyOverrides(
IModel model,
IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
{
foreach (var entityType in model.GetEntityTypes())
{
foreach (var property in entityType.GetDeclaredProperties())
{
var storeObjectOverrides = RelationalPropertyOverrides.Get(property);
if (storeObjectOverrides == null)
{
continue;
}
foreach (var storeObjectOverride in storeObjectOverrides)
{
if (GetAllMappedStoreObjects(property, storeObjectOverride.StoreObject.StoreObjectType)
.Any(o => o == storeObjectOverride.StoreObject))
{
continue;
}
var storeObject = storeObjectOverride.StoreObject;
switch (storeObject.StoreObjectType)
{
case StoreObjectType.Table:
throw new InvalidOperationException(
RelationalStrings.TableOverrideMismatch(
entityType.DisplayName() + "." + property.Name,
storeObjectOverride.StoreObject.DisplayName()));
case StoreObjectType.View:
throw new InvalidOperationException(
RelationalStrings.ViewOverrideMismatch(
entityType.DisplayName() + "." + property.Name,
storeObjectOverride.StoreObject.DisplayName()));
case StoreObjectType.SqlQuery:
throw new InvalidOperationException(
RelationalStrings.SqlQueryOverrideMismatch(
entityType.DisplayName() + "." + property.Name,
storeObjectOverride.StoreObject.DisplayName()));
case StoreObjectType.Function:
throw new InvalidOperationException(
RelationalStrings.FunctionOverrideMismatch(
entityType.DisplayName() + "." + property.Name,
storeObjectOverride.StoreObject.DisplayName()));
case StoreObjectType.InsertStoredProcedure:
case StoreObjectType.DeleteStoredProcedure:
case StoreObjectType.UpdateStoredProcedure:
throw new InvalidOperationException(
RelationalStrings.StoredProcedureOverrideMismatch(
entityType.DisplayName() + "." + property.Name,
storeObjectOverride.StoreObject.DisplayName()));
default:
throw new NotSupportedException(storeObject.StoreObjectType.ToString());
}
}
}
}
}
上面源代码中高亮部分就是抛出异常的地方。有大伙伴会说:老周你这是瞎扯啊,把一个实体映射到多个表,在官方文档上就有,只要看过文档的都不会犯这个错误。老周为了介绍其背后的知识,所以故意虚构了这个故事嘛。
好了,咱们简单说说原因。这里有一个概念,叫做 Property Override。说人话就是实体属性到数据列的映射可以存在覆盖关系。通常,咱们通过 PropertyBuilder 配置的列名、列的数据类型等是调用扩展方法 HasColumnXXXXX,例如
modelBuilder.Entity<Pet>(entity =>
{
entity.Property(c => c.PetId).HasColumnName("pet_id");
......
});
实际上它是在代表属性的元数据上直接添加名为 Relational:ColumnName 的 Annotation(这个可以翻译为"注释")。Annotations 本质上是一个以字符串为 key,以 object 为 value 的字典结构。EF Core 中许多元数据都是用 Annotation 的方式存储的。再比如,你在 EntityTypeBuilder 上调用 ToTable 扩展方法,所配置的数据表名称,是以 Relational:TableName 的Key存入 Annotation 字典中的。就像这样
Model:
EntityType: Pet
Properties:
PetId (int) Required PK FK AfterSave:Throw ValueGenerated.OnAdd
Annotations:
Relational:ColumnName: pet_id
SqlServer:ValueGenerationStrategy: IdentityColumn
Category (string) MaxLength(15)
Annotations:
MaxLength:15
SqlServer:ValueGenerationStrategy: None
Color (string) MaxLength(12)
Annotations:
MaxLength: 12
SqlServer:ValueGenerationStrategy: None
Hobbies (string[]) Required MaxLength(100) Element type: string Required
Annotations:
ElementType: Element type: string Required
MaxLength: 100
SqlServer:ValueGenerationStrategy: None
Length (int?)
Annotations:
SqlServer:ValueGenerationStrategy: None
NickName (string) Required MaxLength(20)
Annotations:
MaxLength: 20
SqlServer:ValueGenerationStrategy: None
Temperament (string) MaxLength(30)
Annotations:
MaxLength: 30
SqlServer:ValueGenerationStrategy: None
Weight (float?)
Annotations:
SqlServer:ValueGenerationStrategy: None
Keys:
PetId PK
Annotations:
Relational:Name: PK_my_pet
Foreign keys:
Pet {'PetId'} -> Pet {'PetId'} Unique Required Cascade
Annotations:
Relational:Name: FK_petid
Annotations:
Relational:FunctionName:
Relational:Schema:
Relational:SqlQuery:
Relational:TableName: Pet
Relational:ViewName:
Relational:ViewSchema:
Annotations:
ProductVersion: 10.0.1
Relational:MaxIdentifierLength: 128
SqlServer:ValueGenerationStrategy: IdentityColumn
但是,在 ToTable 方法调用时,如果使用 TableBuilder 的 HasColumnName 方法所配置的列名,并不是保存到 key 为 Relational:ColumnName 的 Annotation 字典中的。咱们不妨验证一下。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Pet>(entity =>
{
......
entity.ToTable("tb_pet", tb =>
{
tb.Property(x => x.PetId).HasColumnName("pet_id");
tb.Property(x => x.NickName).HasColumnName("name");
tb.Property(x => x.Category).HasColumnName("cate");
});
......
}
/*--------------------------------------------------------------------------------*/
using TestContext context = new();
// 获得设计时模型
IDesignTimeModel dsmodelsvc = context.GetService<IDesignTimeModel>();
IModel dsmodel = dsmodelsvc.Model;
// 枚举出每个实体,每个实体的属性中的 Annotations
foreach(var entity in dsmodel.GetEntityTypes())
{
Console.WriteLine($"实体:{entity.DisplayName()}");
foreach(var prop in entity.GetProperties())
{
Console.WriteLine($" {prop.Name}的注释:");
foreach(var anno in prop.GetAnnotations())
{
Console.WriteLine($" {anno.Name}= {anno.Value}");
}
}
}
运行的结果如下:
实体:Pet
PetId的注释:
Relational:ColumnName= pet_id
Relational:RelationalOverrides= Microsoft.EntityFrameworkCore.Metadata.StoreObjectDictionary`1[Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalPropertyOverrides]
SqlServer:ValueGenerationStrategy= IdentityColumn
Category的注释:
MaxLength= 15
Relational:RelationalOverrides= Microsoft.EntityFrameworkCore.Metadata.StoreObjectDictionary`1[Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalPropertyOverrides]
Color的注释:
MaxLength= 12
Hobbies的注释:
ElementType= Element type: string Required
MaxLength= 100
ValueConverter=
ValueConverterType=
............
有没有发现多了个 Key 为 Relational:RelationalOverrides 的注释项?而且它是个 StoreObjectDictionary 类型的字典。它的声明如下:
public class StoreObjectDictionary<T> : Microsoft.EntityFrameworkCore.Metadata.IReadOnlyStoreObjectDictionary<T> where T : class
在这里,T 是 RelationalPropertyOverrides 类,这个类在用途上不对外公开(位于 Microsoft.EntityFrameworkCore.Metadata.Internal 命名空间),看命名空间就知道这货是和元数据有关的。其中,这个类公开了 SetColumnName 方法,设置的列名存放在 _columnName 字段中。
1、调用 EntityTypeBuilder 的 ToTable 扩展方法时,可得到 TableBuilder;
2、从 TableBuilder 的 Property 方法返回得到一个 ColumnBuilder 对象;
3、调用 ColumnBuilder 对象的 HasColumnName 方法,这个方法调用了上面 RelationalPropertyOverrides 类的 SetColumnName 方法。
所以,你每调用一次 ToTable 方法,并用 TableBuilder 对象配置一次列名,那么 StoreObjectDictionary 字典里就会多一个 RelationalPropertyOverrides 元素。
咱们继续实验,把前面的代码改一下,专门打印 RelationalOverrides 注释的内容。
#pragma warning disable EF1001
namespace WTF;
internal class Program
{
static void Main(string[] args)
{
using TestContext context = new();
// 获得设计时模型
IDesignTimeModel dsmodelsvc = context.GetService<IDesignTimeModel>();
IModel dsmodel = dsmodelsvc.Model;
// 枚举出每个实体
foreach(var entity in dsmodel.GetEntityTypes())
{
Console.WriteLine($"实体:{entity.DisplayName()}");
foreach(var prop in entity.GetProperties())
{
var anno = prop.FindAnnotation(RelationalAnnotationNames.RelationalOverrides);
var dics = anno?.Value as StoreObjectDictionary<RelationalPropertyOverrides>;
if(dics != null)
{
foreach(var item in dics.GetValues())
{
Console.WriteLine($" {item.DebugView.LongView}");
}
}
}
}
}
}
先用 FindAnnotation 方法查找出各个属性中的 RelationalOverrides 注释,然后把注释的值转换为 StoreObjectDictionary<RelationalPropertyOverrides> 字典,最后枚举字典中的项。
运行结果如下:
实体:Pet
Override: tb_pet ColumnName: pet_id
Override: tb_pet ColumnName: cate
Override: tb_pet ColumnName: name
如果调用 ToTable 方法映射三个表,RelationalOverrides 字典中的项就会增加。由于模型验证会导致异常,咱们写一个验证服务类,暂时忽略掉对属性覆盖的验证。
public class MyModelValidator : RelationalModelValidator
{
// 构造函数的参数不用管,往基类传就是了,它是靠依赖注入取值的
public MyModelValidator(
ModelValidatorDependencies dependencies,
RelationalModelValidatorDependencies relationalDependencies)
: base(dependencies, relationalDependencies)
{
}
// 重写需要忽略的成员
protected override void ValidatePropertyOverrides(IModel model, IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
{
// 直接返回,不执行基类的代码
return;
//base.ValidatePropertyOverrides(model, logger);
}
}
然后在数据库上下文类的 OnConfiguring 方法中替换默认服务。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("server=...")
.ReplaceService<IModelValidator, MyModelValidator>();
}
在实际开发中可不要这么干,这样容易破坏原有的验证逻辑。
这时候我们让 Pet 实体映射成三个表。
entity.ToTable("tb_pet", tb =>
{
tb.Property(x => x.PetId).HasColumnName("pet_id");
tb.Property(x => x.NickName).HasColumnName("name");
tb.Property(x => x.Category).HasColumnName("cate");
});
entity.ToTable("tb_pet_chars", tb =>
{
tb.Property(p => p.PetId).HasColumnName("_pid");
tb.Property(p => p.Weight).HasColumnName("weight");
tb.Property(p => p.Length).HasColumnName("len");
tb.Property(p => p.Color).HasColumnName("fur_color");
});
entity.ToTable("tb_pet_other", tb =>
{
tb.Property(x => x.PetId).HasColumnName("_pid");
tb.Property(x => x.Temperament).HasColumnName("tempera");
tb.Property(x => x.Hobbies).HasColumnName("hobbies");
});
最后输出的 RelationalOverrides 如下:
实体:Pet
Override: tb_pet ColumnName: pet_id
Override: tb_pet_chars ColumnName: _pid
Override: tb_pet_other ColumnName: _pid
Override: tb_pet ColumnName: cate
Override: tb_pet_chars ColumnName: fur_color
Override: tb_pet_other ColumnName: hobbies
Override: tb_pet_chars ColumnName: len
Override: tb_pet ColumnName: name
Override: tb_pet_other ColumnName: tempera
Override: tb_pet_chars ColumnName: weight
这东西有点复杂,不知道各位看懂了没有。其实就是你调用 ToTable 方法时,如果用 TableBuilder.Property(...).HasColumnName(...) 等方法配置一次,就会在 Overrides 字典里添加一条记录。但是,这个覆盖只针对属性和列之间的映射,而不针对表的。啥意思呢,咱们继补充一下代码,打印出实体中 TableName 注释的值。
static void Main(string[] args)
{
using TestContext context = new();
// 获得设计时模型
IDesignTimeModel dsmodelsvc = context.GetService<IDesignTimeModel>();
IModel dsmodel = dsmodelsvc.Model;
// 枚举出每个实体,每个实体的属性中的 Annotations
foreach (var entity in dsmodel.GetEntityTypes())
{
var tbName = entity.FindAnnotation(RelationalAnnotationNames.TableName)?.Value as string;
Console.Write($"实体:{entity.DisplayName()}");
if (tbName is not (null or { Length: 0 }))
{
Console.Write(" 表名:{0}\n", tbName);
}
else
{
Console.Write("\n");
}
......
}
|
这里其实可以直接调用 GetTableName 方法获取表名的: entity.GetTableName()。
运行后输出的内容如下:
实体:Pet 表名:tb_pet_other
Override: tb_pet ColumnName: pet_id
Override: tb_pet_chars ColumnName: _pid
Override: tb_pet_other ColumnName: _pid
Override: tb_pet ColumnName: cate
Override: tb_pet_chars ColumnName: fur_color
Override: tb_pet_other ColumnName: hobbies
Override: tb_pet_chars ColumnName: len
Override: tb_pet ColumnName: name
Override: tb_pet_other ColumnName: tempera
Override: tb_pet_chars ColumnName: weight
咱们设置表名的顺序是 tb_pet -> tb_chars -> tb_other。而保存表名的就只有一个 Relational:TableName 的 key。也就是说,不管你调用多少次 ToTable 方法,不管你设置了多少个表名,Relational:TableName 键所对应的表名只能是一个------最后设置的那个,因为后面设置的值把旧值替换了。
这个东西不太好讲述,可能老周也讲得不清楚,所以有必总结一下,这个试验到底验证了什么。
1、ToTable 扩展方法设置的表名存到实体的 Relational:TableName 注释中,永远只保留最后设置的表名。
2、TableBuilder 所设置的列名,没有用 Relational:ColumnName 注释去保存,而是新加了一个 Relational:RelationalOverrids 注释,然后以字典形式存储所有覆盖内容,要注意的是,覆盖行为是基于属性,而不是实体的。比如上面例子中的 PetId 属性,它的第一个配置是映射到 tb_pet 表的 pet_id 列;第二个是映射到 tb_chars 表的 _pid 列;第三个是映射到 tb_other 表的 _pid 列。
那么,什么情况下会直接用 Relational:ColumnName 注释存储属性与列的映射呢?答案是调用 PropertyBuilder 的 HasColumnName 方法。就像这样:
modelBuilder.Entity<Pet>(entity =>
{
entity.Property(c => c.PetId).HasColumnName("pet_id");
......
}
可见,这两处的 HasColumnName 方法是完全不一样的,再重复一遍,因为这个怕大伙伴们不好理解,老周只好多点F话。
1、PropertyBuilder.HasColumnName(通过 EntityTypeBuilder.Property(...))直接在属性元数据中写入 Relational:ColumnName 注释。因此,这个 HasColumnName 不管调用多少次,保留都是最后一个设置的值,和 TableName 一样。
2、ColumnBuilder.HasColumnName(通过 ToTable => TableBuilder.Property(...))是在属性元数据上写入 Relational:RelationalOverrides 注释,并且其值是字典集合,你每调用一次 ToTable 它就会往集合里增加一个子项,即属性的列配置可以被覆盖很多次。
到了这里,有大伙伴可能有点悟了,这样不合理啊,实体与表之间的映射应该是唯一的。正是,所以我们开头那个示例就报错了啊,模型验证失败了呢。老周之所以绕了个大圈,现在才解释为啥抛异常,是担心大伙伴们看不懂,只好先说一下原理。我们现在回过头,看看 ValidatePropertyOverrides 方法的源代码。
protected virtual void ValidatePropertyOverrides(
IModel model,
IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
{
// 逐个实体检查
foreach (var entityType in model.GetEntityTypes())
{
// 实体中逐个属性检查
foreach (var property in entityType.GetDeclaredProperties())
{
// 这一行其实是返回 Relational:RelationalOverrides 注释的内容(字典)
// 集合中所有 Override 对象
var storeObjectOverrides = RelationalPropertyOverrides.Get(property);
if (storeObjectOverrides == null)
{
continue; // 如果没有,说明列的配置没有被覆盖
}
// 遍历所有的覆盖配置
foreach (var storeObjectOverride in storeObjectOverrides)
{
// 这里实际上是根据当前属性,找到包含这个属性的实体
// 再根据这个实体,得到它映射的表名,这里读的是 Relational:TableName 注释
// 而现在我们用了三个 ToTable 方法,导致实体映射的表名是 tb_other
// 而 Overrides 集合中,这个属性可能对应了 tb_pet 表或 tb_chars 表
// Any(o => o == storeObjectOverride.StoreObject) 方法的调用就是用来比较 Overrides 中的表名和 TableName 注释中的表名是否相同
if (GetAllMappedStoreObjects(property, storeObjectOverride.StoreObject.StoreObjectType)
.Any(o => o == storeObjectOverride.StoreObject))
{
continue; // 如果存在任意一条是相同的,说明表名一致,就不会报错
}
// 代码走到这里,就说明上面的验证失败了,两处表名不一致
// StoreObjectType 只是表明出错的映射是面向数据表,还是表值函数,还是存储过程
var storeObject = storeObjectOverride.StoreObject;
switch (storeObject.StoreObjectType)
{
case StoreObjectType.Table:
// 示例程序报错的就是这里
throw new InvalidOperationException(
RelationalStrings.TableOverrideMismatch(
entityType.DisplayName() + "." + property.Name,
storeObjectOverride.StoreObject.DisplayName()));
case StoreObjectType.View:
throw new InvalidOperationException(
RelationalStrings.ViewOverrideMismatch(
entityType.DisplayName() + "." + property.Name,
storeObjectOverride.StoreObject.DisplayName()));
......
}
}
}
}
}
我们再看看前面实验代码输出的 overrides 列表。
实体:Pet 表名:Relational:TableName = tb_pet_other
Override: tb_pet ColumnName: pet_id
Override: tb_pet_chars ColumnName: _pid
Override: tb_pet_other ColumnName: _pid
Override: tb_pet ColumnName: cate
Override: tb_pet_chars ColumnName: fur_color
Override: tb_pet_other ColumnName: hobbies
Override: tb_pet_chars ColumnName: len
Override: tb_pet ColumnName: name
Override: tb_pet_other ColumnName: tempera
Override: tb_pet_chars ColumnName: weight
根据源代码,首先是枚举实体,这里只有一个 Pet,然后枚举属性,那第一个就是 PetId 属性,接着枚举 PetId 属性的 Overrides,有三个:
1、映射 tb_pet 表的 pet_id 列;
2、映射 tb_chars 表的 _pid 列;
3、映射 tb_other 表的 _pid 列。
但是,GetAllMappedStoreObjects 方法是根据属性来创建 StoreObjectIdentifier 列表的,在本例中,这个 Identifire 就是 tb_other,这个 foreach 循环的意思就是所有 Override 的属性的表名都必须是 tb_other,如果有一个不是,就抛异常。foreach 循环第一个配置的是 tb_pet 表与 pet_id 列,然而现在的表名是 tb_other,所以,第一轮就匹配失败了,就 throw 了。
这样就保证了一个实体只能 Map 一个表。
二、正确用法
那么,EF Core 用什么办法把一个实体分散到多个表的?它很狡猾,一方面坚持一实体 Map 一表的原则,另一方面,它又提供一个叫"分片"(Fragment)的概念。实体映射的主表存储在 RelationalOverrides 注释中,而将其余分表存储在名为 Relational:MappingFragments 的注释中,同理,它也是一个字典集合------ StoreObjectDictionary<EntityTypeMappingFragment>。一个分片由 EntityTypeMappingFragment 类表示,对外暴露三个接口:IEntityTypeMappingFragment、IMutableEntityTypeMappingFragment 和 IConventionEntityTypeMappingFragment。即
public class EntityTypeMappingFragment :
ConventionAnnotatable,
IEntityTypeMappingFragment,
IMutableEntityTypeMappingFragment,
IConventionEntityTypeMappingFragment
{
......
}
配置分片表调用的是 SplitToTable 扩展方法。和 TableBuilder 一样,属性与列的映射可以覆盖,并保存到 RelationalOverrides 注释中,只不过多了个 MappingFragments 注释。但多了这个分片,在模型验证时就不同了,GetAllMappedStoreObjects 方法中会循环遍历 Fragments 集合,并返回集合中所有表名。
if (property.IsPrimaryKey()) // 对于主键
{
// 这个是对非分片的表
var declaringStoreObject = StoreObjectIdentifier.Create(property.DeclaringType, storeObjectType);
if (declaringStoreObject != null)
{
yield return declaringStoreObject.Value;
}
// 表值函数,或数据来源于 SQL 查询,终止
if (storeObjectType is StoreObjectType.Function or StoreObjectType.SqlQuery)
{
yield break;
}
// 这里就针对分片,分片集合中所有表名都返回
foreach (var fragment in property.DeclaringType.GetMappingFragments(storeObjectType))
{
yield return fragment.StoreObject;
}
// 当前实体的派生类也要返回(TPT 或 TPC 映射方式)
// 如果是 TPH 映射,基类子类都存放在一个表中,只返回一个
if (property.DeclaringType is IReadOnlyEntityType entityType)
{
foreach (var containingType in entityType.GetDerivedTypes())
{
var storeObject = StoreObjectIdentifier.Create(containingType, storeObjectType);
if (storeObject != null)
{
yield return storeObject.Value;
// TPH 映射就是基类实体和它的派生类全存放在一个表中,并用一个专用列来标识类型,所以它不再需要返回其他表名,故中止
if (mappingStrategy == RelationalAnnotationNames.TphMappingStrategy)
{
yield break;
}
}
}
}
}
else // 对于非主键
{
// 获取当前属性中 TableName 注释所配置的表名,或默认表名
var declaringStoreObject = StoreObjectIdentifier.Create(property.DeclaringType, storeObjectType);
// 表值函数和SQL查询的结果不需要多个表
if (storeObjectType is StoreObjectType.Function or StoreObjectType.SqlQuery)
{
if (declaringStoreObject != null)
{
yield return declaringStoreObject.Value;
}
yield break;
}
if (declaringStoreObject != null)
{
// 枚举所有分片
var fragments = property.DeclaringType.GetMappingFragments(storeObjectType).ToList();
if (fragments.Count > 0)
{
// 只要 Overrides 中的任意一列与分片中的表名匹配,都返回
var overrides = RelationalPropertyOverrides.Find(property, declaringStoreObject.Value);
if (overrides != null)
{
yield return declaringStoreObject.Value;
}
foreach (var fragment in fragments)
{
overrides = RelationalPropertyOverrides.Find(property, fragment.StoreObject);
if (overrides != null)
{
yield return fragment.StoreObject;
}
}
yield break;
}
// 要是没有配置分片,说明只映射一个表,返回它
yield return declaringStoreObject.Value;
if (mappingStrategy != RelationalAnnotationNames.TpcMappingStrategy)
{
yield break;
}
}
if (property.DeclaringType is not IReadOnlyEntityType entityType)
{
yield break;
}
// 对于当前实体的派生类
// 1、如果是TPH映射模式,那么全程只用一个表,所以只返回一个就够了
// 2、TPC模式即每个派生类都要有一个表,所以全部返回
var tableFound = false;
var queue = new Queue<IReadOnlyEntityType>();
queue.Enqueue(entityType);
while (queue.Count > 0 && !tableFound)
{
// 枚举直接派生类,不含间接子类
// TPC模式下,当前实体可能是抽象类
foreach (var containingType in queue.Dequeue().GetDirectlyDerivedTypes())
{
// 获取派生类实体配置的表名
var storeObject = StoreObjectIdentifier.Create(containingType, storeObjectType);
if (storeObject != null)
{
yield return storeObject.Value; // 至少返回一个
tableFound = true;
// TPH 映射模式下只需要一个表就行了,所以 break
if (mappingStrategy == RelationalAnnotationNames.TphMappingStrategy)
{
yield break;
}
}
// 如果是 TPC 模式且找不到被映射的表,此时 containingType 可能是抽象类
// 把抽象类扔回队列中,下一轮循环继续撸它的派生类
if (!tableFound
|| mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy)
{
queue.Enqueue(containingType);
}
}
}
}
经过这么一处理,在 ValidatePropertyOverrides 方法中,只要任意一个 Override 的列的表名和分片中的表名匹配,就验证成功。这么一搞,就做到了一个实体可以 Map 多个表了。
于是,数据库上下文类里面,OnModelCreating 方法的代码你应该知道怎么改了吧。
modelBuilder.Entity<Pet>(entity =>
{
entity.Property(c => c.PetId).HasColumnName("pet_id");
// 文本类型的配置一下长度,不然全是 MAX 也不划算
entity.Property(d => d.NickName).HasMaxLength(20);
entity.Property(d => d.Color).HasMaxLength(12);
entity.Property(d => d.Category).HasMaxLength(15);
entity.Property(d => d.Hobbies).HasMaxLength(100);
entity.Property(d => d.Temperament).HasMaxLength(30);
// 给主键命个名
entity.HasKey(d => d.PetId).HasName("PK_my_pet");
// 第一个表是主表,配置不变
entity.ToTable("tb_pet", tb =>
{
tb.Property(x => x.PetId).HasColumnName("pet_id");
tb.Property(x => x.NickName).HasColumnName("name");
tb.Property(x => x.Category).HasColumnName("cate");
});
// 第二个表
entity.SplitToTable("tb_pet_chars", tb =>
{
tb.Property(p => p.PetId).HasColumnName("_pid");
tb.Property(p => p.Weight).HasColumnName("weight");
tb.Property(p => p.Length).HasColumnName("len");
tb.Property(p => p.Color).HasColumnName("fur_color");
});
// 第三个表
entity.SplitToTable("tb_pet_other", tb =>
{
tb.Property(x => x.PetId).HasColumnName("_pid");
tb.Property(x => x.Temperament).HasColumnName("tempera");
tb.Property(x => x.Hobbies).HasColumnName("hobbies");
});
// 配置外键
entity.HasOne<Pet>()
.WithOne()
.HasForeignKey<Pet>(p => p.PetId)
.HasConstraintName("FK_petid");
});
第一个表是主表,ToTable 保持不变;第二、三个表调用 SplitToTable 方法,列映射不需要改。
现在,把前面咱们替换的 IModelValidator 接口还原。在 OnConfiguring 方法中删除 ReplaceService 方法的调用。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("server=...");
//.ReplaceService<IModelValidator, MyModelValidator>();
}
重新运行示例,现在不会报错了。也可以用以下代码打印一下各个分片的信息。
internal class Program
{
static void Main(string[] args)
{
using TestContext context = new();
// 获得设计时模型
IDesignTimeModel dsmodelsvc = context.GetService<IDesignTimeModel>();
IModel dsmodel = dsmodelsvc.Model;
// 枚举出每个实体,每个实体的属性中的 Annotations
foreach (var entity in dsmodel.GetEntityTypes())
{
// 获取表名也可以不用查找 TableName 注释,直接用 GetTableName 方法即可
var tbName =entity.GetTableName();
Console.Write($"实体:{entity.DisplayName()}");
if (tbName is not (null or { Length: 0 }))
{
Console.Write(" 表名:{0}\n", tbName);
}
else
{
Console.Write("\n");
}
foreach (var prop in entity.GetProperties())
{
// 打印 overrides 的更简单方法,不用查找 RelationalOverrides 注释
var overrides = prop.GetOverrides();
foreach(var ovr in overrides)
{
Console.WriteLine($" {ovr.ToDebugString()}");
}
}
// 打印分片
Console.WriteLine("\n 分片:");
foreach(var fragment in entity.GetMappingFragments())
{
Console.WriteLine($" {fragment.ToDebugString()}");
}
}
}
}
由于 EF 有相关的扩展方法,其实咱们不需要去手动查找注释的,如 GetTableName 方法获取表名,GetOverrides 方法获属性的覆盖配置,GetMappingFragments 方法获取分片列表。
再次运行示例,结果如下:
实体:Pet 表名:tb_pet
Override: tb_pet ColumnName: pet_id
Override: tb_pet_chars ColumnName: _pid
Override: tb_pet_other ColumnName: _pid
Override: tb_pet ColumnName: cate
Override: tb_pet_chars ColumnName: fur_color
Override: tb_pet_other ColumnName: hobbies
Override: tb_pet_chars ColumnName: len
Override: tb_pet ColumnName: name
Override: tb_pet_other ColumnName: tempera
Override: tb_pet_chars ColumnName: weight
分片:
Fragment: tb_pet_chars
Fragment: tb_pet_other
咱们不妨获取一下创建数据表的 SQL 语句,检查一下是否正确。在 Main 方法结束之前放入以下代码。
string sql = context.Database.GenerateCreateScript();
Console.WriteLine("\n\n创建数据表SQL:\n{0}", sql);
生成的 SQL 语句如下:
CREATE TABLE [tb_pet] (
[pet_id] int NOT NULL IDENTITY,
[name] nvarchar(20) NOT NULL,
[cate] nvarchar(15) NULL,
CONSTRAINT [PK_my_pet] PRIMARY KEY ([pet_id])
);
GO
CREATE TABLE [tb_pet_chars] (
[_pid] int NOT NULL,
[weight] real NULL,
[len] int NULL,
[fur_color] nvarchar(12) NULL,
CONSTRAINT [PK_my_pet] PRIMARY KEY ([_pid]),
CONSTRAINT [FK_petid] FOREIGN KEY ([_pid]) REFERENCES [tb_pet] ([pet_id]) ON DELETE CASCADE
);
GO
CREATE TABLE [tb_pet_other] (
[_pid] int NOT NULL,
[hobbies] nvarchar(100) NOT NULL,
[tempera] nvarchar(30) NULL,
CONSTRAINT [PK_my_pet] PRIMARY KEY ([_pid]),
CONSTRAINT [FK_petid] FOREIGN KEY ([_pid]) REFERENCES [tb_pet] ([pet_id]) ON DELETE CASCADE
);
GO
所有表的主键名称都统一为咱们所配置的 PK_my_pet。只有主表 tb_pet 的主键使用 IDENTITY 生成标识,其他的分表不使用自动生成,而是与主表相同的主键值。同时,分表都有一个外键 FK_petid,引用主表的主键。这个外键对应的列同时也是当前分表的主键。
这样可以保证在数据操作中,三个表的状态能保持一致。
好了,今天就聊到这儿了。这次的内容有点复杂,可能不太好懂,老周也没法保证能讲明白。如果弄不懂也不要紧,会用 SplitToTable 来拆表就行。