宿迁市网站建设_网站建设公司_Logo设计_seo优化
2025/12/26 17:38:28 网站建设 项目流程

把一个实体类型映射到多个表,官方叫法是 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: IdentityColumnCategory (string) MaxLength(15) Annotations:MaxLength:15SqlServer: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_petidAnnotations: Relational:FunctionName: Relational:Schema: Relational:SqlQuery:Relational:TableName: PetRelational: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,引用主表的主键。这个外键对应的列同时也是当前分表的主键。

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

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

立即咨询