Entity Framework 6.2 从一个DbContext多对多复制到另一个DbContext

Entity Framework 6.2 copy many to many from one DbContext to another DbContext

当使用网络数据库(例如 MySQL)时,DbContext 应该是短暂的,但根据 https://www.entityframeworktutorial.net/EntityFramework4.3/persistence-in-entity-framework.aspx,当使用本地数据库(例如 SQLite)时,DbContext 可以是长期存在的.

我的应用程序使用长期存在的 DbContext 与 HDD 上的 SQLite 一起工作,我想将多对多实体复制到另一个 DbContext 以用于 USB 上相同类型的 SQLite 数据库。

我正在使用代码优先方法。

public class Student
{
    public Student() 
    {
        this.Courses = new HashSet<Course>();
    }

    public int StudentId { get; set; }
    [Required]
    public string StudentName { get; set; }

    public virtual ICollection<Course> Courses { get; set; }
}

public class Course
{
    public Course()
    {
        this.Students = new HashSet<Student>();
    }

    public int CourseId { get; set; }
    public string CourseName { get; set; }

    public virtual ICollection<Student> Students { get; set; }
}

DbContextHDD 包含学生 StudentA、StudentB 和 StudentC 以及课程 Course1、Course2 和 Course3:

StudentA attends Course1 and Course3
StudentB attends Course2 and Course3
StudentC attends Course1 and Course2

DbContextUSB 不包含学生和课程。

var courses = DbContextHDD.Courses.AsNoTracking();
List<Student> students = new List<Student>();
foreach(Course course in courses)
{
    foreach(Student student in course.Students)
    {
        if(!students.Any(s => s.StudentId == student.StudentId))
        {
            students.Add(student);
        }
    }
}
Debug.WriteLine(students.Count); // output: 3

Debug.WriteLine(DbContextUSB.Students.Local.Count); // output: 0
DbContextUSB.Students.AddRange(students);
Debug.WriteLine(DbContextUSB.Students.Local.Count); // output: 4
DbContextUSB.SaveChanges(); // exception: UNIQUE constraint failed

DbContextUSB.Courses.AddRange(courses);
DbContextUSB.SaveChanges();

为什么我将 3 个不同的学生插入到一个有 0 个学生的 DbSet 后,有 4 个学生(3 个唯一的和 1 个重复的)?这样做的正确方法是什么?

正如我所说,我正在使用长期使用的 DbContext,因为我正在使用 SQLite。

首先,不要使用AsNoTracking:

var courses = DbContextHDD.Courses. ...

二、Include所需资料:

var courses = DbContextHDD.Courses
    .Include(c => c.Students)
    .ToList();

三、将课程添加到其他上下文中:

DbContextUSB.Courses.AddRange(courses);
DbContextUSB.SaveChanges();

你可能不相信,但本质上就是这样!

一个警告是您应该在源上下文中禁用代理创建:

DbContextHDD.Configuration.ProxyCreationEnabled = false;

否则 EF 会创建代理对象,这些对象引用了它们来自的上下文。它们不能附加到另一个上下文。

另一个是可能会有学生不上课。查询课程时您会想念它们。所以你必须单独添加它们:

var lazyStudents = DbContextHDD.Students.Where(s => s.Courses.Count() == 0).ToList();
...
DbContextUSB.Students.AddRange(lazyStudents);
...
DbContextUSB.SaveChanges();

为什么这样做?

  • 没有跟踪,Entity Framework 无法检测到 StudentA 在 Course1 与 Course3 中的学生相同。结果,学生 A 在 Course3 中是一个新的 Student 实例。您最终将有 6 个学生,3 个重复(如果 StudentName 上没有唯一索引阻止这种情况)。通过跟踪,EF 确实检测到 两门课程都有相同的 Student 实例。

  • 将实体添加到上下文时,EF 还会标记嵌套 Added 尚未附加到上下文的实体。 这就是为什么只添加课程就足够了,这就是为什么EF不添加课程的原因 当课程包含相同的学生实例时抱怨。

  • 由于添加的课程已正确填充其 Students 集合,因此 EF 还会在 StudentCourse table 中插入所需的联结记录。这并没有发生在您的代码中(也许,或者部分地,稍后见)。

现在为什么你有 4 个学生?

查看课程:

Course1 StudentA*, StudentC*
Course2 StudentB*, StudentC
Course3 StudentA , StudentB

因为 AsNoTracking 所有学生都是不同的实例,但只有标记*的学生在 students 中,因为你如何添加他们。但这是棘手的部分。即使使用 AsNoTracking(),Entity Framework 也会对在一个查询中具体化的相关实体执行 关系修正 。这意味着 foreach(Course course in courses) 循环生成具有填充 Students 集合的课程,其中每个学生在其 Courses 集合中有一门课程。几乎不可能跟踪到底发生了什么,尤其是。因为调试也会触发延迟加载,但可以肯定的是,行...

DbContextUSB.Students.AddRange(students);

还将他们的嵌套课程和 他们的 学生标记为 Added,只要他们最终成为不同的实例。在这种情况下,最终结果是将另一个学生实例添加到缓存中。此外,创建了许多联结记录,但不一定是正确的。

结论是 EF 是克隆对象图的好工具,但必须正确填充图、正确的关系并且没有重复项,并且应该一次性添加。