EF多对多关系和数据重复

时间:2016-03-10 23:27:40

标签: entity-framework many-to-many

我遇到EF(6.1.3)

的问题

我创建了下一个类(具有多对多关系):

public class Record
{
    [Key]
    public int RecordId { get; set; }

    [Required]
    public string Text { get; set; }

    public virtual ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    [Key]
    public int TagId { get; set; }

    [Required]
    public string Name { get; set; }

    public virtual ICollection<Record> Records{ get; set; }
}

方法:

void AddTags()
{
    Record[] records;

    Tag[] tags;

    using (var context = new AppDbContext())
    {
        records = context.Records.ToArray();
    }//remove line to fix

    tags = Enumerable.Range(0, 5).Select(x => new Tag()
        {
            Name = string.Format("Tag_{0}", x),
            Records= records.Skip(x * 5).Take(5).ToArray()
        }).ToArray();

    using (var context = new AppDbContext()){ //remove line to fix
        context.Tags.AddRange(tags);
        context.SaveChanges();
    }
}

如果我使用两个上下文,则会复制记录(添加到创建的标记中)。如果我删除标记的行 - 问题就会消失。

有没有办法在不使用相同的上下文的情况下修复此问题?

1 个答案:

答案 0 :(得分:0)

如果可以,可以更好地重新加载实体或根本不分离它们。在应用程序中使用多个上下文实例总体上使事情变得更加复杂。

您遇到的问题来自实体框架实体更改跟踪器。当您从DbContext加载权限并处置该上下文时,实体将与实体更改跟踪器分离,并且实体框架不知道对其进行的任何更改。

在您通过附加实体引用分离实体后,它(分离实体)会立即进入实体更改跟踪器,并且它不知道之前已加载此实体。为了让实体框架知道这个分离的实体来自数据库,你必须重新附加它:

foreach (var record in records) {
    dbContext.Entry(record).State = EntityState.Unchanged;
}

这样您就可以使用记录在其他对象中引用,但如果您对这些记录进行了任何更改,那么所有这些更改都将消失。要对数据库进行更改,您必须将状态更改为已添加:

dbContext.Entry(record).State = EntityState.Modified;

实体框架使用您的映射来确定数据库中要应用更改的行,特别是使用您的主键设置。

几个例子:

public class Bird
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    public string Color { get; set; }
}

public class Tree
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
}

public class BirdOnATree
{
    [Column(Order = 0), Key, ForeignKey("Bird")]
    public int BirdId { get; set; }
    public Bird Bird { get; set; }

    [Column(Order = 1), Key, ForeignKey("Tree")]
    public int TreeId { get; set; }
    public Tree Tree { get; set; }
    public DateTime SittingSessionStartedAt { get; set; }
}

这是一个小实体结构,以便您可以看到它是如何工作的。你可以看到Bird和Tree有简单的Key - Id。 BirdOnATree是鸟对树对的多对多表,附加列SittingSessionStartedAt

以下是多个上下文的代码:

Bird bird;

using (var context = new TestDbContext())
{
    bird = context.Birds.First();
}

using (var context = new TestDbContext())
{
    var tree = context.Trees.First();

    var newBirdOnAtree = context.BirdsOnTrees.Create();
    newBirdOnAtree.Bird = bird;
    newBirdOnAtree.Tree = tree;
    newBirdOnAtree.SittingSessionStartedAt = DateTime.UtcNow;

    context.BirdsOnTrees.Add(newBirdOnAtree);
    context.SaveChanges();
}

在这种情况下,鸟从DB上脱落而没有再次附着。实体框架会将此实体视为一个新实体,即使Id属性设置为指向现有行到数据库,它也不会存在于DB中。要更改此项,您只需将此行添加到开头的第二个DbContext:

context.Entry(bird).State = EntityState.Unchanged;

如果执行此代码,它将不会在DB中创建新的Bird实体,而是使用现有代码。

第二个例子:我们不是从数据库中获取鸟类,而是由我们自己创建:

bird = new Bird
    {
        Id = 1,
        Name = "Nightingale", 
        Color = "Gray"
    }; // these data are different in DB

执行时,此代码也不会创建另一个鸟类实体,将在BirdOnATree表中引用Id = 1的鸟类,并且不会更新Id = 1的鸟类实体。事实上,您可以在此处放置任何数据,只需使用正确的ID。

如果我们在此更改代码以使此分离实体更新DB中的现有行:

context.Entry(bird).State = EntityState.Modified;

这样,正确的数据将被插入到表BirdOnATree中,但是在表Bird中也会更新Id = 1的行,以适合您在应用程序中提供的数据。

您可以查看有关对象状态跟踪的文章:

https://msdn.microsoft.com/en-US/library/dd456848(v=vs.100).aspx

总的来说,如果可以避免这种情况,请不要使用对象状态跟踪和相关代码。它可能会出现难以找到源的不需要的更改 - 当您不期望它们时,字段会更新为实体,或者在您预期时不会更新。