【问题标题】:Entity Framework 4.1+ many-to-many relationships change trackingEntity Framework 4.1+ 多对多关系变更跟踪
【发布时间】:2011-09-20 19:15:00
【问题描述】:

如何检测 ICollection 属性的变化(多对多关系)?

public class Company
{
    ...

    public virtual ICollection<Employee> Employees { get; set; }
}

using (DataContext context = new DataContext(Properties.Settings.Default.ConnectionString))
{
    Company company = context.Companies.First();
    company.Employees.Add(context.Employees.First());

    context.SaveChanges();
}

public class DataContext : DbContext
{
    public override int SaveChanges()
    {
        return base.SaveChanges();

        // Company's entity state is "Unchanged" in this.ChangeTracker
    }
}

【问题讨论】:

  • 其实是个好问题。 ObjectStateManager 返回的条目比 DbContext 的 ChangeTracker 多,尤其是“关系”类型的条目。例如:((IObjectContextAdapter)DbContext).ObjectContext.ObjectStateManager .GetObjectStateEntries(EntityState.Added) 返回一个 ObjectStateEntry,它表示您的示例中添加的关系。但我不知道如何从这里开始,这个条目中有趣的数据(父实体和添加的子实体)在调试器中是可见的,但都是私有/内部的......
  • 我不确定您所说的“检测 ICollection 属性的更改”是什么意思。如果您的意思是查看 SaveChanges 内部将进行哪些更改,那么我可以回答这个问题,但这会很复杂,因为正如@Slauma 所暗示的,多对多关系始终是独立的关联。如果您的意思是,如何让 EF 从 DetectChanges 的意义上检测这些更改,那么这应该已经在代码中发生了。公司实体的状态没有改变,因为根据独立关联,它没有改变,只是关系发生了变化。
  • 谢谢亚瑟!我认为问题是如何在 SaveChanges 中为多对多关系获取隐藏连接表中添加(或删除)的记录。我知道这很复杂,但我的项目需要它。例如,在我的应用程序中,用户可以关注其他用户,当有人关注(或取消)其他人时,我需要将事件记录到数据库中。并且有很多多对多关系,我需要找到一个明确的记录所有这些关系的解决方案。你能给我一个小代码演示吗?谢谢!

标签: entity-framework many-to-many change-tracking


【解决方案1】:

以下是如何找到所有已更改的多对多关系。我已经将代码实现为扩展方法:

public static class IaExtensions
{
    public static IEnumerable<Tuple<object, object>> GetAddedRelationships(
        this DbContext context)
    {
        return GetRelationships(context, EntityState.Added, (e, i) => e.CurrentValues[i]);
    }

    public static IEnumerable<Tuple<object, object>> GetDeletedRelationships(
        this DbContext context)
    {
        return GetRelationships(context, EntityState.Deleted, (e, i) => e.OriginalValues[i]);
    }

    private static IEnumerable<Tuple<object, object>> GetRelationships(
        this DbContext context,
        EntityState relationshipState,
        Func<ObjectStateEntry, int, object> getValue)
    {
        context.ChangeTracker.DetectChanges();
        var objectContext = ((IObjectContextAdapter)context).ObjectContext;

        return objectContext
            .ObjectStateManager
            .GetObjectStateEntries(relationshipState)
            .Where(e => e.IsRelationship)
            .Select(
                e => Tuple.Create(
                    objectContext.GetObjectByKey((EntityKey)getValue(e, 0)),
                    objectContext.GetObjectByKey((EntityKey)getValue(e, 1))));
    }
}

一些解释。多对多关系在 EF 中表示为独立关联或 IA。这是因为关系的外键没有暴露在对象模型的任何地方。在数据库中,FK 位于连接表中,并且此连接表对对象模型隐藏。

在 EF 中使用“关系条目”跟踪 IA。这些类似于您从 DbContext.Entry 获得的 DbEntityEntry 对象,只是它们表示两个实体之间的关系而不是实体本身。关系条目没有在 DbContext API 中公开,因此您需要下拉到 ObjectContext 才能访问它们。

在创建两个实体之间的新关系时会创建一个新的关系条目,例如通过将 Employee 添加到 Company.Employees 集合中。此关系处于已添加状态。

同样,当删除两个实体之间的关系时,关系条目将进入已删除状态。

这意味着要查找更改的多对多关系(或实际上任何更改的 IA),我们需要查找添加和删除的关系条目。这就是 GetAddedRelationships 和 GetDeletedRelationships 的作用。

一旦我们有了关系条目,我们就需要理解它们。为此,您需要了解一些内幕知识。添加(或未更改)关系条目的 CurrentValues 属性包含两个值,它们是关系两端实体的 EntityKey 对象。同样,但稍有不同的是,Deleted 关系条目的 OriginalValues 属性包含已删除关系两端实体的 EntityKey 对象。

(而且,是的,这太可怕了。请不要责怪我——这是在我的时代之前。)

CurrentValues/OriginalValues 的区别是我们将委托传递给 GetRelationships 私有方法的原因。

一旦我们有了 EntityKey 对象,我们就可以使用 GetObjectByKey 来获取实际的实体实例。我们将这些作为元组返回,然后就可以了。

这里有一些实体、一个上下文和一个初始化器,我曾经对此进行测试。 (注意——测试并不广泛。)

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Employee> Employees { get; set; }

    public override string ToString()
    {
        return "Company " + Name;
    }
}

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Company> Companies { get; set; }

    public override string ToString()
    {
        return "Employee " + Name;
    }
}

public class DataContext : DbContext
{
    static DataContext()
    {
        Database.SetInitializer(new DataContextInitializer());
    }

    public DbSet<Company> Companies { get; set; }
    public DbSet<Employee> Employees { get; set; }

    public override int SaveChanges()
    {
        foreach (var relationship in this.GetAddedRelationships())
        {
            Console.WriteLine(
                "Relationship added between {0} and {1}",
                relationship.Item1,
                relationship.Item2);
        }

        foreach (var relationship in this.GetDeletedRelationships())
        {
            Console.WriteLine(
                "Relationship removed between {0} and {1}",
                relationship.Item1,
                relationship.Item2);
        }

        return base.SaveChanges();
    }

}

public class DataContextInitializer : DropCreateDatabaseAlways<DataContext>
{
    protected override void Seed(DataContext context)
    {
        var newMonics = new Company { Name = "NewMonics", Employees = new List<Employee>() };
        var microsoft = new Company { Name = "Microsoft", Employees = new List<Employee>() };

        var jim = new Employee { Name = "Jim" };
        var arthur = new Employee { Name = "Arthur" };
        var rowan = new Employee { Name = "Rowan" };

        newMonics.Employees.Add(jim);
        newMonics.Employees.Add(arthur);
        microsoft.Employees.Add(arthur);
        microsoft.Employees.Add(rowan);

        context.Companies.Add(newMonics);
        context.Companies.Add(microsoft);
    }
}

这是一个使用它的例子:

using (var context = new DataContext())
{
    var microsoft = context.Companies.Single(c => c.Name == "Microsoft");
    microsoft.Employees.Add(context.Employees.Single(e => e.Name == "Jim"));

    var newMonics = context.Companies.Single(c => c.Name == "NewMonics");
    newMonics.Employees.Remove(context.Employees.Single(e => e.Name == "Arthur"));

    context.SaveChanges();
} 

【讨论】:

  • 这个答案是伟大的亚瑟!关于这个棘手的主题的最好的。我希望我能给你一个以上的支持!
  • 谢谢!你为我节省了很多时间)
  • 谢谢@arthur。它工作得很好。只是一个问题:参数 Func 有什么作用?它是否允许函数在上下文中调用此函数?我对这个参数有点困惑。非常感谢!
  • 考虑将 DbContext 更改为 ObjectContext。这对 EF4.0 开发人员也会有所帮助。
  • 太棒了!无论如何要拒绝这些上下文变化?我使用本地 DbSets 集合在 WPF 中进行绑定,但我需要拒绝多对多的关系更改...
【解决方案2】:

我不能为您的情况提供确切的代码,但我可以告诉您,通过在员工和公司之间建立一个联接表,您的情况将简化十倍,只是为了打破多对多关系。

【讨论】:

  • 完全正确,上一个答案请查看我的笔记
猜你喜欢
  • 1970-01-01
  • 2012-10-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多