EF4 Code First - проблема отношения многих ко многим

У меня возникли некоторые проблемы с моей моделью EF Code First при сохранении отношения "многие ко многим". Мои модели:

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }  
    public virtual ICollection<Tag> Tags { get; set; }
}


public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Event> Events { get; set; }
}

В моем контроллере я сопоставляю одну или несколько моделей TagViewModel с типом тега и отправляю их на уровень обслуживания для сохранения. В настоящее время при проверке сущностей у тега есть и идентификатор, и имя (идентификатор - это скрытое поле, а имя - это текстовое поле, на мой взгляд)

Проблема возникает, когда я сейчас пытаюсь добавить тег к событию. Давайте рассмотрим следующий сценарий:

Событие уже находится в моей базе данных, и, скажем, оно уже имеет связанные теги C#, ASP.NET

Если я сейчас отправлю следующий список тегов на слой обслуживания:

ID  Name
1   C#
2   ASP.NET
3   EF4

и добавить их, сначала извлекая Событие из БД, чтобы у меня было фактическое Событие из моего DbContext, затем я просто делаю

myEvent.Tags.Add

добавить теги.. Проблема в том, что после SaveChanges() моя БД теперь содержит этот набор тегов:

ID  Name
1   C#
2   ASP.NET
3   EF4
4   C#
5   ASP.NET

Это, несмотря на то, что у моих Тегов, которые я сохраняю, есть свой ID, установленный при сохранении (хотя я не получил его из БД)

2 ответа

Решение

Я думаю, я знаю, что произошло в вашем коде. Позвольте мне объяснить мое мнение на простом примере:

using (var context = new Context())
{
    // Just let assume these are your tags received from view model.
    var csharp = ...;
    var aspnet = ...;
    var ef4 = ...;

    // Now you load Event
    var e = context.Events.Where(e => e.Id == someId);
    // Ups first access to Tag collection triggers lazy loading which
    // is enabled by default so, all current tags are loaded
    e.Tags.Add(csharp);
    e.Tags.Add(aspnet);
    e.Tags.Add(ef4);

    // Now e.Tags.Count == 5 !!! Why?
    context.SaveChanges();
}

Первая проблема: потому что динамический прокси создан поверх вашего Event экземпляр использует HashSet за Tags он проверяет, существует ли добавленная сущность в коллекции. Если нет, он добавляет объект, иначе пропускает объект. Чтобы иметь возможность сделать эту проверку правильно, ВЫ ДОЛЖНЫ РЕАЛИЗОВАТЬ Equals и GetHashCode в Tag! Так как вы этого не сделали, он принимает добавленные теги как новые с временными ключами и добавляет их в Tags стол с автоматически сгенерированным ключом.

Вторая проблема: даже если вы реализуете Equals а также GetHashCode Вы решите только двойственность тегов C# и ASP.NET. На данный момент контекст не отслеживает тег EF4, поэтому этот тег все еще считается новым. Вы должны сообщить контексту, что тег EF4 существует в БД. Итак, начнем Attach все теги в контексте, прежде чем вы начнете ленивую загрузку Tags коллекция. Прикрепление объекта к контексту по умолчанию устанавливает его состояние Unchanged:

using (var context = new Context())
{
    foreach (var tag in TagsFromView)
    {
        context.Attach(tag);
    }

    // Now you load Event
    var e = context.Events.Where(e => e.Id == someId);
    foreach(var tag in TagsFromView)
    {
        // First access will trigger lazy loading but already
        // attached instances of tags are used
        e.Tags.Add(tag);
    }

    // Now you must delete all tags present in e.Tags and not
    // present in TagsFromView

    context.SaveChanges();
}

Это работает, если вы не создаете новые теги в своем представлении. Если вы хотите сделать это, вы не должны прикреплять новые теги к контексту. Вы должны различать существующие теги и новые теги (например, новые теги могут иметь Id = 0).

Вам нужно получить теги из базы данных, иначе EF будет рассматривать их как новые элементы и переопределять идентификатор.

Другие вопросы по тегам