Entity Splitting场景下外键引用子表属性的报错解决咨询
我正在基于现有定制数据库创建新的IdentityDbContext。多年前决定不在AspNetUsers表中添加自定义属性,而是将这些属性放到另一个表CustomUser中,该表拥有独立的标识列UserId。UserId被用作CustomUserAccept表的外键,该表用于记录用户接受服务条款、Cookie政策等的时间。
public void CreateCustomUser(EntityTypeBuilder<CustomUser> entity) { entity .ToTable("AspNetUsers") .SplitToTable("CustomUser", CreateCustomUser); entity.HasAlternateKey(e => e.UserId); entity.HasMany(d => d.Accepts).WithMany() .UsingEntity<CustomUserAccept>( "CustomUserAccept", r => r.HasOne(d => d.Accept).WithMany() .HasForeignKey(d => d.AcceptId) .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_CustomUserAccept_AcceptId"), l => l.HasOne(d => d.User).WithMany() .HasPrincipalKey(d => d.UserId) .HasForeignKey(d => d.UserId) .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_CustomUserAccept_UserId"), CreateCustomUserAccept); } public void CreateCustomUser(SplitTableBuilder<CustomUser> entity) { entity.Property(e => e.UserId).UseIdentityColumn(); entity.Property(e => e.Id).HasColumnName("IdentityId"); entity.Property(e => e.CompanyId); entity.Property(e => e.FirstName); entity.Property(e => e.LastName); entity.Property(e => e.LastLogonDate); entity.Property(e => e.PasswordExpirationDate); entity.Property(e => e.ChangePassword); } public void CreateCustomUserAccept(EntityTypeBuilder<CustomUserAccept> entity) { entity.HasKey(e => new { e.UserId, e.AcceptId }); entity.ToTable("CustomUserAccept"); entity.Property(e => e.CreatedDate) .HasDefaultValueSql("(getutcdate())") .HasColumnType("datetime"); }
添加与CustomAccept的多对多引用后,出现以下错误:
'Microsoft.EntityFrameworkCore.Model.Validation.ForeignKeyPropertiesMappedToUnrelatedTables': 实体类型'CustomUserAccept (CustomUserAccept)'上指向'CustomUser'的外键{'UserId'}无法在数据库中表示。要么属性{'UserId'}未映射到表'CustomUserAccept',要么主体属性{'UserId'}未映射到表'AspNetUsers'。所有外键属性必须映射到依赖类型所在的表,所有主体属性必须映射到主体类型所在的单个表。
请问是否有办法绕过“主体属性必须映射到单个表”的限制?比如通过操作元数据或重写?除了放弃Entity Splitting之外,我还能采取什么措施?
解决方案
1. 手动修改模型元数据(绕过验证)
EF Core的模型验证逻辑可以通过直接操作元数据调整,尽管这属于非官方用法,需注意版本兼容性。在OnModelCreating方法的最后阶段添加以下代码:
// 获取CustomUserAccept的外键信息 var userAcceptEntity = modelBuilder.Model.FindEntityType(typeof(CustomUserAccept)); var userIdProperty = userAcceptEntity.FindProperty(nameof(CustomUserAccept.UserId)); var foreignKey = userAcceptEntity.FindForeignKey(new[] { userIdProperty }); if (foreignKey != null) { // 将UserId主体属性的映射表指定为CustomUser var principalUserIdProp = foreignKey.PrincipalKey.Properties.Single(p => p.Name == nameof(CustomUser.UserId)); principalUserIdProp.SetTableName("CustomUser"); }
这段代码会强制EF Core认为UserId属性属于CustomUser表,从而通过验证。
2. 替换多对多为显式双向一对多
放弃自动生成的多对多关联,改为显式配置两个一对多关系,这是更稳定的官方兼容方案:
首先在CustomUserAccept实体中添加导航属性:
public class CustomUserAccept { public int UserId { get; set; } public int AcceptId { get; set; } public DateTime CreatedDate { get; set; } public CustomUser User { get; set; } public Accept Accept { get; set; } }
然后修改模型配置:
// 替换原CreateCustomUser中的多对多配置 public void CreateCustomUser(EntityTypeBuilder<CustomUser> entity) { entity .ToTable("AspNetUsers") .SplitToTable("CustomUser", CreateCustomUser); entity.HasAlternateKey(e => e.UserId); // 配置CustomUser到CustomUserAccept的一对多 entity.HasMany(d => d.Accepts) .WithOne(ua => ua.User) .HasForeignKey(ua => ua.UserId) .HasPrincipalKey(u => u.UserId) .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_CustomUserAccept_UserId"); } // 单独配置Accept到CustomUserAccept的一对多 public void ConfigureAccept(EntityTypeBuilder<Accept> entity) { entity.HasMany(a => a.UserAccepts) .WithOne(ua => ua.Accept) .HasForeignKey(ua => ua.AcceptId) .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_CustomUserAccept_AcceptId"); }
这种方式完全符合EF Core的模型验证规则,不会有版本兼容风险。
3. 调整拆分实体的主键策略
如果业务允许,可以将CustomUser的主键改为UserId,并将AspNetUsers的Id作为普通属性映射:
public void CreateCustomUser(EntityTypeBuilder<CustomUser> entity) { // 将UserId设为CustomUser的主键 entity.HasKey(e => e.UserId); // 主表设为CustomUser entity.ToTable("CustomUser"); // 拆分AspNetUsers为副表,映射Id为普通属性 entity.SplitToTable("AspNetUsers", b => { b.Property(e => e.Id).HasColumnName("Id"); // 配置AspNetUsers的其他Identity相关属性 }); // 后续关联配置保持不变,此时UserId属于主表CustomUser,满足验证要求 }
这种方式需要调整实体的主键逻辑,可能会影响Identity相关的默认操作,需要充分测试后再采用。
内容的提问来源于stack exchange,提问作者Rich Bennema

