EF多对多关系和数据重复

EF many-to-many relationship and data duplication

我在使用 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();
    }
}

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

有没有办法在不使用相同上下文的情况下解决这个问题?

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

您的问题来自 Entity Framework 实体更改跟踪器。当您从 DbContext 加载实体并处理该上下文时,实体会与实体更改跟踪器分离,并且 Entity Framework 不知道对其所做的任何更改。

在你通过附加实体引用分离实体后,它(分离实体)立即进入实体更改跟踪器,它不知道这个实体之前被加载过。要让 Entity Framework 知道这个分离的实体来自数据库,您必须重新附加它:

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

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

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

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

几个例子:

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 是一个多对多 table 的 Bird-Tree 对,带有附加列 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();
}

在这种情况下,bird 已从 DB 中分离出来并且不再附加。 Entity Framework 会将此实体视为一个新实体,它在数据库中从未存在过,即使 Id 属性 设置为指向数据库的现有行。要更改它,您只需将此行添加到第二个 DbContext 的开头:

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

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

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

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

执行时,此代码也不会创建另一个鸟实体,将在 BirdOnATree table 中引用 Id = 1 的鸟,并且不会更新 Id = 1 的鸟实体。实际上你可以在这里放任何数据,只要使用正确的ID。

如果我们在此处更改代码以使这个分离的实体更新数据库中的现有行:

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

这样,正确的数据将插入到 table BirdOnATree,而且 ID = 1 的行将在 table Bird 中更新以适应您在申请。

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

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

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