来宾市网站建设_网站建设公司_动画效果_seo优化
2025/12/26 12:35:45 网站建设 项目流程

把一个实体类型映射到多个表,官方叫法是 Entity splitting,这个称呼有点难搞,要是翻译为“实体拆分”或“拆分实体”,你第一感觉会不会认为是把一个表拆分为多个实体的意思。可它的含义是正好相反。为了避免大伙伴们产生误解,老周直接叫它“一个实体映射到多个表”,虽然不言简,但很意赅。

把一个实体类对应到数据库中的多个表,本质上是啥呢?一对一,是不是?举个例子,看图。

image

恭喜你猜对了,正如上图所示,假设老周收了几个徒弟,上述三个表其实都是【学生】实体类拆开的。第一个表是学生的基础信息,第二个表是补充信息,第三个表是学生的联系方式。第二、三个表中的行必须与第一个表中的行一一对应。

基于这样的理解,咱们可以得出:第一个表有主键A,第二个表有个外键FA引用主键A,第三个表有个外键FB引用主键A。同时,考虑到第二、三个表中的数据是完全依赖第一个表的,所以,第二、三个表中可以把主键和外键设定为同一个列。说人话就是有一列既做当前表的主键,也做外键引用第一个表。这使得第二、三个表中每一条记录的主键列的值必须与第一个表中的主键列相同。

image

 

下面咱们举个例子说明一下。假设有这样一个实体。

/// <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");});
}

映射了三个表,最后创建一个外键,指向主键——自己引用自己。代码看着挺合理,但运行会报错。

image

错误是在模型验证过程中发生的,即验证失败。该异常是在 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: PetProperties:PetId (int) Required PK FK AfterSave:Throw ValueGenerated.OnAddAnnotations:Relational:ColumnName: pet_idSqlServer:ValueGenerationStrategy: IdentityColumnCategory (string) MaxLength(15)Annotations:MaxLength: 15SqlServer:ValueGenerationStrategy: NoneColor (string) MaxLength(12)Annotations:MaxLength: 12SqlServer:ValueGenerationStrategy: NoneHobbies (string[]) Required MaxLength(100) Element type: string RequiredAnnotations:ElementType: Element type: string RequiredMaxLength: 100SqlServer:ValueGenerationStrategy: NoneLength (int?)Annotations:SqlServer:ValueGenerationStrategy: NoneNickName (string) Required MaxLength(20)Annotations:MaxLength: 20SqlServer:ValueGenerationStrategy: NoneTemperament (string) MaxLength(30)Annotations:MaxLength: 30SqlServer:ValueGenerationStrategy: NoneWeight (float?)Annotations:SqlServer:ValueGenerationStrategy: NoneKeys:PetId PKAnnotations:Relational:Name: PK_my_petForeign keys:Pet {'PetId'} -> Pet {'PetId'} Unique Required CascadeAnnotations:Relational:Name: FK_petidAnnotations:Relational:FunctionName:Relational:Schema:Relational:SqlQuery:Relational:TableName: PetRelational:ViewName:Relational:ViewSchema:
Annotations:ProductVersion: 10.0.1Relational:MaxIdentifierLength: 128SqlServer: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}");}}
}

运行的结果如下:

实体:PetPetId的注释:Relational:ColumnName= pet_idRelational:RelationalOverrides= Microsoft.EntityFrameworkCore.Metadata.StoreObjectDictionary`1[Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalPropertyOverrides]SqlServer:ValueGenerationStrategy= IdentityColumnCategory的注释:MaxLength= 15Relational:RelationalOverrides= Microsoft.EntityFrameworkCore.Metadata.StoreObjectDictionary`1[Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalPropertyOverrides]Color的注释:MaxLength= 12Hobbies的注释:ElementType= Element type: string RequiredMaxLength= 100ValueConverter=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 EF1001namespace 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> 字典,最后枚举字典中的项。

运行结果如下:

实体:PetOverride: tb_pet ColumnName: pet_idOverride: tb_pet ColumnName: cateOverride: 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 如下:

实体:PetOverride: tb_pet ColumnName: pet_idOverride: tb_pet_chars ColumnName: _pidOverride: tb_pet_other ColumnName: _pidOverride: tb_pet ColumnName: cateOverride: tb_pet_chars ColumnName: fur_colorOverride: tb_pet_other ColumnName: hobbiesOverride: tb_pet_chars ColumnName: lenOverride: tb_pet ColumnName: nameOverride: tb_pet_other ColumnName: temperaOverride: 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;// 枚举出每个实体,每个实体的属性中的 Annotationsforeach (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_otherOverride: tb_pet ColumnName: pet_idOverride: tb_pet_chars ColumnName: _pidOverride: tb_pet_other ColumnName: _pidOverride: tb_pet ColumnName: cateOverride: tb_pet_chars ColumnName: fur_colorOverride: tb_pet_other ColumnName: hobbiesOverride: tb_pet_chars ColumnName: lenOverride: tb_pet ColumnName: nameOverride: tb_pet_other ColumnName: temperaOverride: 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_otherOverride: tb_pet ColumnName: pet_idOverride: tb_pet_chars ColumnName: _pidOverride: tb_pet_other ColumnName: _pidOverride: tb_pet ColumnName: cateOverride: tb_pet_chars ColumnName: fur_colorOverride: tb_pet_other ColumnName: hobbiesOverride: tb_pet_chars ColumnName: lenOverride: tb_pet ColumnName: nameOverride: tb_pet_other ColumnName: temperaOverride: 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 映射模式下只需要一个表就行了,所以 breakif (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;// 枚举出每个实体,每个实体的属性中的 Annotationsforeach (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_petOverride: tb_pet ColumnName: pet_idOverride: tb_pet_chars ColumnName: _pidOverride: tb_pet_other ColumnName: _pidOverride: tb_pet ColumnName: cateOverride: tb_pet_chars ColumnName: fur_colorOverride: tb_pet_other ColumnName: hobbiesOverride: tb_pet_chars ColumnName: lenOverride: tb_pet ColumnName: nameOverride: tb_pet_other ColumnName: temperaOverride: tb_pet_chars ColumnName: weight分片:Fragment: tb_pet_charsFragment: 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])
);
GOCREATE 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
);
GOCREATE 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 来拆表就行。

 

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询