Entity Framework 4.1+ отслеживание изменений отношений "многие ко многим"

Как я могу обнаружить изменения свойств 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
    }
}

person Boris Mitchenko    schedule 20.09.2011    source источник
comment
На самом деле хороший вопрос. ObjectStateManager возвращает больше записей, чем ChangeTracker из DbContext, особенно также записи типа Relationship. Например: ((IObjectContextAdapter)DbContext).ObjectContext.ObjectStateManager .GetObjectStateEntries(EntityState.Added) возвращает один ObjectStateEntry, который представляет добавленную связь в вашем примере. Но я не могу понять, как действовать дальше, интересные данные в этой записи (родительская и добавленная дочерняя сущность) видны в отладчике, но все частные / внутренние ...   -  person Slauma    schedule 21.09.2011
comment
Я не уверен, что вы имеете в виду, говоря об обнаружении изменений свойств ICollection ‹›. Если вы имеете в виду посмотреть, какие изменения будут внесены в SaveChanges, то я могу ответить на этот вопрос, но это будет сложно, потому что отношения «многие ко многим» всегда являются независимыми ассоциациями, как намекает @Slauma. Если вы имеете в виду, как вы можете заставить EF обнаруживать эти изменения в смысле DetectChanges, тогда это уже должно происходить в коде. Состояние компании не изменилось, потому что в соответствии с независимыми ассоциациями не изменилось, изменились только отношения.   -  person Arthur Vickers    schedule 18.03.2012
comment
Спасибо, Артур! Я думаю, что вопрос в том, как получить добавленную (или удаленную) запись в скрытой таблице соединений для отношения «многие ко многим» в SaveChanges. Я знаю, что это сложно, но моему проекту это нужно. Например, в моем приложении пользователь может подписываться на других пользователей, когда кто-то подписывается (или отменяет) кого-то еще, мне нужно зарегистрировать событие в базе данных. И есть много отношений «многие-ко-многим», мне нужно найти четкое решение для регистрации всех этих отношений. Не могли бы вы дать мне небольшую демонстрацию кода для этого? Спасибо!   -  person Chance    schedule 19.03.2012


Ответы (2)


Вот как найти все изменившиеся отношения «многие ко многим». Я реализовал код как методы расширения:

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 находятся в таблице соединения, и эта таблица соединения скрыта от объектной модели.

IA отслеживаются в EF с помощью «записей отношений». Они похожи на объекты DbEntityEntry, которые вы получаете из DbContext.Entry, за исключением того, что они представляют связь между двумя сущностями, а не саму сущность. Записи отношений не отображаются в DbContext API, поэтому для доступа к ним необходимо перейти к ObjectContext.

Новая запись отношения создается при создании новой связи между двумя сущностями, например, путем добавления сотрудника в коллекцию Company.Employees. Эта связь находится в состоянии «Добавлено».

Аналогичным образом, когда связь между двумя объектами удаляется, запись связи переводится в состояние «Удалено».

Это означает, что для поиска измененных отношений "многие ко многим" (или фактически любого измененного IA) нам необходимо найти добавленные и удаленные записи отношений. Это то, что делают GetAddedRelationships и GetDeletedRelationships.

Когда у нас есть записи отношений, нам нужно разобраться в них. Для этого вам нужно знать частичку инсайдерских знаний. Свойство CurrentValues ​​записи отношения Added (или Unchanged) содержит два значения, которые являются объектами EntityKey сущностей на любом конце отношения. Точно так же, но немного отличается, свойство OriginalValues ​​записи отношения Deleted содержит объекты 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();
} 
person Arthur Vickers    schedule 23.03.2012
comment
Это отличный ответ, Артур! Лучшее в этой сложной теме. Хотел бы я дать вам больше одного голоса! - person Hannish; 30.01.2013
comment
Спасибо! Вы сэкономили мне кучу времени) - person white.zaz; 08.05.2013
comment
Спасибо @arthur. Работает очень хорошо. Просто вопрос: что делает параметр Func ‹ObjectStateEntry, int, object›? Позволяет ли функция вызывать эту функцию в контексте? Я немного запутался в этом параметре. Большое спасибо! - person Rafael Merlin; 12.11.2014
comment
Рассмотрите возможность изменения DbContext на ObjectContext. Это также будет полезно для разработчиков EF4.0. - person Davut Gürbüz; 11.01.2015
comment
Большой! Есть ли способ отклонить эти изменения в контексте? Я использую локальные коллекции DbSets для привязки в WPF, но мне нужно отклонить многие и многие изменения отношений ... - person Hesam Kashefi; 30.11.2017
comment
@Arthur Vickers Это действительно круто. Но как использовать кортеж с двумя объектами? Что можно делать с предметами? - person onefootswill; 31.05.2018
comment
Это отличный ответ, но не с большими контекстами, это потребует много памяти !!! Пробовали на продакшене. А наш Контекст, к сожалению, огромен! При выполнении ((IObjectContextAdapter)context).ObjectContext выбросить OutOfMemoryException! - person bunjeeb; 19.10.2020

Я не могу дать вам точный код для вашей ситуации, но могу сказать, что ваша ситуация будет упрощена в десять раз, если будет объединен стол между Сотрудниками и Компанией, чтобы разрушить отношения «многие ко многим».

person Isuru Fonseka    schedule 13.11.2011
comment
это абсолютно верно, проверьте мою заметку для предыдущего ответа - person bunjeeb; 19.10.2020