EF代码优先模式下如何用单个外键关联多个主键?
嘿,先得给你提个醒:你想要的「单个ObjectId列同时关联Professor、College、EducationalGroup三个表主键」的SQL约束写法,在关系型数据库里其实是不合法也无法正常工作的——因为数据库会要求每条ObjectScore的ObjectId必须同时存在于这三个表中,这显然不符合你的业务逻辑对吧?
不过别担心,我们可以通过两种合理的方案来实现你想要的「分数关联不同类型实体」的业务需求,下面给你详细说明:
方案一:拆分外键列(推荐,符合数据库规范)
这是最稳妥的做法,给每个关联实体单独设置外键列,逻辑清晰,数据完整性也有保障:
用数据注解修改ObjectScore类
直接在实体类上标记外键和导航属性:
public class ObjectScore { public Guid Id { get; set; } public int CurrentScore { get; set; } public virtual Term Term { get; set; } public virtual Score Score { get; set; } // 分别为每个关联实体添加可空外键和导航属性 public int? ProfessorId { get; set; } [ForeignKey(nameof(ProfessorId))] public virtual Professor Professor { get; set; } public int? CollegeId { get; set; } [ForeignKey(nameof(CollegeId))] public virtual College College { get; set; } public int? EducationalGroupId { get; set; } [ForeignKey(nameof(EducationalGroupId))] public virtual EducationalGroup EducationalGroup { get; set; } }
用Fluent API配置(如果你更喜欢这种方式)
在你的DbContext的OnModelCreating方法里添加以下配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 关联Professor modelBuilder.Entity<ObjectScore>() .HasOne(s => s.Professor) .WithMany(p => p.ObjectScores) .HasForeignKey(s => s.ProfessorId) .OnDelete(DeleteBehavior.Restrict); // 根据你的业务需求设置删除行为 // 关联College modelBuilder.Entity<ObjectScore>() .HasOne(s => s.College) .WithMany(c => c.ObjectScores) .HasForeignKey(s => s.CollegeId) .OnDelete(DeleteBehavior.Restrict); // 关联EducationalGroup modelBuilder.Entity<ObjectScore>() .HasOne(s => s.EducationalGroup) .WithMany(eg => eg.ObjectScores) .HasForeignKey(s => s.EducationalGroupId) .OnDelete(DeleteBehavior.Restrict); }
这种方案的好处是每个外键独立生效,数据库能自动保证数据的合法性,后续维护也更简单。
方案二:多态关联(模拟单个ObjectId关联多实体)
如果你坚持要使用单个ObjectId列,我们可以添加一个鉴别器字段来区分这个Id对应的是哪个实体,用EF的Fluent API来配置这种多态关联:
第一步:修改ObjectScore类,添加鉴别器
public class ObjectScore { public Guid Id { get; set; } public int CurrentScore { get; set; } public virtual Term Term { get; set; } public virtual Score Score { get; set; } public int ObjectId { get; set; } // 鉴别器字段:用来标记ObjectId对应的实体类型,比如填"Professor"、"College"或"EducationalGroup" public string ObjectType { get; set; } }
第二步:用Fluent API配置关联
在DbContext的OnModelCreating中配置组合键关联:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置与Professor的关联,基于(ObjectType, ObjectId)组合键 modelBuilder.Entity<ObjectScore>() .HasOne<Professor>() .WithMany(p => p.ObjectScores) .HasPrincipalKey(p => p.Id) .HasForeignKey(s => new { s.ObjectType, s.ObjectId }) .HasConstraintName("FK_ObjectScore_Professor") .IsRequired(false) .OnDelete(DeleteBehavior.Restrict); // 配置与College的关联 modelBuilder.Entity<ObjectScore>() .HasOne<College>() .WithMany(c => c.ObjectScores) .HasPrincipalKey(c => c.Id) .HasForeignKey(s => new { s.ObjectType, s.ObjectId }) .HasConstraintName("FK_ObjectScore_College") .IsRequired(false) .OnDelete(DeleteBehavior.Restrict); // 配置与EducationalGroup的关联 modelBuilder.Entity<ObjectScore>() .HasOne<EducationalGroup>() .WithMany(eg => eg.ObjectScores) .HasPrincipalKey(eg => eg.Id) .HasForeignKey(s => new { s.ObjectType, s.ObjectId }) .HasConstraintName("FK_ObjectScore_EducationalGroup") .IsRequired(false) .OnDelete(DeleteBehavior.Restrict); }
注意:这种方式下,数据库的外键约束是基于(ObjectType, ObjectId)组合的,你需要在业务代码里保证ObjectType的取值准确,并且ObjectId在对应的实体表中存在——EF不会自动帮你做这个校验,得自己加逻辑。
再说说你原始SQL约束的问题
你写的那三个FK约束在SQL里是无法执行的,因为数据库会要求每条ObjectScore的ObjectId必须同时存在于Professor、College、EducationalGroup三个表中,这几乎不可能满足,所以必须调整设计哦。
内容的提问来源于stack exchange,提问作者Oyama

