【问题标题】:EF Core, How to update a record in table which has both One To Many and Many to Many relationships with the same entityEF Core,如何更新与同一实体具有一对多和多对多关系的表中的记录
【发布时间】:2021-05-06 13:31:39
【问题描述】:
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int MainCourseId { get; set; }
    public Course MainCourse { get; set; }
    public IList<StudentCourse> StudentCourses { get; set; }
}

public class Course
{
    public int Id { get; set; }
    public string CourseName { get; set; }
    public string Description { get; set; }
}

public class StudentCourse
{
    public int StudentId { get; set; }
    public Student Student { get; set; }
    public int CourseId { get; set; }
    public Course Course { get; set; }
}

有两个问题。 第一个问题是,上面的迁移没有创建 MainCourseId ,给出了这个错误。 “在表 'Students' 上引入 FOREIGN KEY 约束 'FK_Students_Courses_MainCourseId' 可能会导致循环或多个级联路径。指定 ON DELETE NO ACTION 或 ON UPDATE NO ACTION,或修改其他 FOREIGN KEY 约束。 无法创建约束或索引。查看以前的错误。”

其次,在另一个实体结构相同的系统中,使用 MainCourseId 中的修改值更新现有记录不起作用。

【问题讨论】:

    标签: c# entity-framework .net-core entity-framework-core ef-core-5.0


    【解决方案1】:

    您的数据库结构有错误。您还需要将 StudentCourse 列表添加到课程课程中。恕我直言,这不是我见过的最好的数据库结构。更好的方法是将 Id 添加到 StudentCourse 表中,如果它是主课程,则添加一个标志。

    public class StudentCourse
    {
        public int Id { get; set; }
        public int StudentId { get; set; }
        public Student Student { get; set; }
        public int CourseId { get; set; }
        public Course Course { get; set; }
        public bool IsMainCourse {get; set;}
    }
    

    而且由于您使用的是 net5,因此您可以在类中添加另一个列表

    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public virtual List<Course> Courses { get; set; }
        public virtual List<StudentCourse> StudentCourses { get; set; }
    }
    
    public class Course
    {
        public int Id { get; set; }
        public string CourseName { get; set; }
        public string Description { get; set; }
        public virtual List<Student> Students{ get; set; }
        public virtual List<StudentCourse> StudentCourses { get; set; }
    }
    

    您需要进行新的初始化迁移才能使用此结构

    【讨论】:

    • “您的数据库结构有错误。您还需要将 StudentCourse 列表添加到 Course 类” 不,您不需要。集合导航属性不是强制性的。问题完全不同。建议的带有标志的设计虽然很好:-)
    • 是的。使用标志很好,因为它要求 MainCourse 是学生的课程之一,并且您可以在 (StudentId,IsMainCourse) 上设置唯一约束以强制学生最多拥有一个 MainCourse。
    • 这强调存在 THE RELATIONSHIP 的属性,而不是任何一个实体。是(所有课程)的“主菜”吗?不,它是学生和课程组合的主要课程,然后“额外”继续加入实体。现在,IsMainCourse 的位置是正确的。一般来说,我认为“布尔”标志几乎总是短视的。一个公开的短 StudentCourseStatus ...,值可能是 UNKNOWN(0)、Main(1)、Secondary(2),但您可以为其他人提供未来的证明。从长远来看,布尔标志几乎总是让你失望。
    • StudentCourse 的其他可能名称可能是“StudentCourseLink”或“StudentToCourse”等#namesAreHard,但这个想法是正确的。有简单的 M:N,并且在 RELATIONSHIP M:N's 上有一些属性.. 我已经学会了继续编写 StudentCourse(Link) 对象的困难.. 一开始......因为有人会过来询问关于这种关系的财产..但我赞成这个建议/答案。
    • @IvanStoev 我同意你的看法。但最好有这个。您还记得几天前我们与您讨论了一个由于多对多关系表处于阴影中而无法进行正确查询的情况。
    【解决方案2】:

    您不能使用创建多个级联路径的级联删除。此处删除课程将删除该课程的所有 StudentCourse 以及将其作为 MainCourse 的所有学生。

    但如果您的 MainCourse 是可选的,则 EF 不会配置 CascadeDelete,因为它可能应该使您能够存储没有 MainCourse 的 Student。因此,要解决这两个问题,只需通过将类型更改为 int? 来使 Student.MainCourseId 成为可选。:

    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int? MainCourseId { get; set; }
        public Course MainCourse { get; set; }
        public IList<StudentCourse> StudentCourses { get; set; }
    }
    

    或者您可以根据需要保留它,并将关系配置为不级联删除。

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>()
                    .HasOne(s => s.MainCourse)
                    .WithMany()
                    .OnDelete(DeleteBehavior.Restrict);
    
        modelBuilder.Entity<StudentCourse>()
                    .HasKey(e => new { e.StudentId, e.CourseId });
    
        base.OnModelCreating(modelBuilder);
    }
    

    我认为使用“跳过导航属性”可能更方便,因此学生有课程,课程有学生,您可以在 EF Core 5+ 中使用多对多映射执行此操作,如下所示:

    using Microsoft.EntityFrameworkCore;
    using System.Linq;
    using System;
    using System.Collections.Generic;
    
    namespace EfCore6Test
    { 
    
    
         public class Student
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public int? MainCourseId { get; set; }
            public Course MainCourse { get; set; }
            public ICollection<Course> Courses { get; } = new HashSet<Course>();
        }
    
        public class Course
        {
            public int Id { get; set; }
            public string CourseName { get; set; }
            public string Description { get; set; }
            public ICollection<Student> Students { get; } = new HashSet<Student>();
        }
    
        public class StudentCourse
        {
            public int StudentId { get; set; }
            public Student Student { get; set; }
            public int CourseId { get; set; }
            public Course Course { get; set; }
        }
    
        public class Db: DbContext
        {
            public DbSet<Student> Students { get; set; }
            public DbSet<Course> Courses{ get; set; }
            public DbSet<StudentCourse> StudentCourses { get; set; }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<Student>().HasOne(s => s.MainCourse).WithMany().OnDelete(DeleteBehavior.Restrict);
                modelBuilder.Entity<Student>().HasMany(s => s.Courses).WithMany(c => c.Students).UsingEntity<StudentCourse>(
                    sc => sc.HasOne(e => e.Course).WithMany(),
                    sc => sc.HasOne(e => e.Student).WithMany()
                    ); 
    
                base.OnModelCreating(modelBuilder);
            }
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseSqlServer("Server=localhost;database=efCore6Test;Integrated Security=true;TrustServerCertificate=true", o => o.UseRelationalNulls(true))
                    .LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);
                base.OnConfiguring(optionsBuilder);
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                {
                    
                    using var db = new Db();
                    db.Database.EnsureDeleted();
                    db.Database.EnsureCreated();
                    var joe = new Student() {Name="Joe" };
                    var math = new Course() { CourseName = "Math" };
                    var english = new Course() { CourseName = "English" };
    
    
                    joe.Courses.Add(math);
                    joe.Courses.Add(english);
    
                    joe.MainCourse = english; 
    
                    db.Students.Add(joe);
    
                    db.SaveChanges();
    
                }
    
                {
    
                    using var db = new Db();
                    var joe = db.Students.Where(s => s.Name == "Joe").First();
                    var math = db.Courses.Where(c => c.CourseName == "Math").First();
                    joe.MainCourse = math;
    
                    db.SaveChanges();
                }
    
            }
        }
    }
    

    【讨论】:

    • “您应该使用 EF Core 5 中的多对多功能” 为什么?在 v5 和任何未来版本中,使用带有显式连接实体的“旧式”多对多是完全可以的。这与原始问题有何关系?问题甚至不是多对多(一种或另一种方式),而是由所需的Student.MainCourseId FK 表示的额外多对一,应该在那里解决。
    • M2M 简化了消费代码,因为您不必在大多数情况下使用 StudentCourse 实体。我将 Student>MainCourse 翻转为可选的,因为可能有一个没有课程的学生,但要求它不会破坏任何东西。
    • 是的,可以选择。但是多对多建议只是固执己见,因此您应该简单地将其删除。这两种方法都有利有弊,而且都不能适用于所有场景。究竟什么是“大多数”,有什么证明吗?隐式更容易阅读,但添加/删除链接要困难得多,如果根本无法加载您并不真正需要的“另一端”实体的话。
    • 文档中没有使用模型中出现的映射实体的示例,但未在任何导航属性中使用。我认为这是 M2M 的一个有用的应用。是的,这是固执己见,但就像使用 HashSet&lt;T&gt; 初始化集合导航属性一样,这只是在修复错误时改进的建议。否则这个问题只是may cause cycles or multiple cascade paths 问题的另一个重复。
    • 如果问题实际上上述问题的另一个重复,那有什么问题?并且应该这样关闭而不是回答。所有这些固执己见的陈述和多余的一堆代码只是隐藏了实际的修复(直到你在我的第一条评论的回复中提到你才注意到它)。因为仅仅使 FK 可以为空就使得关系是可选的,因此没有级联删除,因此没有多个级联路径,因此不需要进一步的操作。我怀疑任何未来的读者(包括 OP)都会注意到这一点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-11-11
    • 2019-03-06
    • 1970-01-01
    • 2017-04-08
    • 2017-10-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多