实体框架6.2从一个DbContext复制许多副本到另一个DbContext

时间:2019-07-24 10:39:45

标签: c# entity-framework sqlite entity-framework-6 many-to-many

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

我的应用程序使用的DbContext寿命长,可以在HDD上使用SQLite,并且我想将USB上相同类型的SQLite数据库的多对多实体复制到另一个DbContext。

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

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个重复)?正确的方法是什么?

正如我所说,由于我使用的是SQLite,因此我使用的DbContext寿命长。

1 个答案:

答案 0 :(得分:1)

首先,不要使用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是同一学生。结果,StudentA Course3中的一个是新的Student实例。您最终将有6个学生,3个重复项(如果StudentName上没有唯一索引会阻止这样做)。通过跟踪,EF可以检测 这两个课程都具有相同的Student实例。

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

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

现在为什么要招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是克隆对象图的好工具,但是必须正确填充图,正确的关系且没有重复,并且应一次性添加。