Как засеять данные связями многие-ко-м в Entity Framework Migrations

Я использую миграцию entity framework (в автоматическом режиме миграции). Все нормально, но у меня один вопрос:

Как мне засеять данные, когда у меня есть отношения «многие ко многим»?

Например, у меня есть два класса моделей:

public class Parcel
{
    public int Id { get; set; }
    public string Description { get; set; }
    public double Weight { get; set; }
    public virtual ICollection<BuyingItem> Items { get; set; }
}

public class BuyingItem
{
    public int Id { get; set; }
    public decimal Price { get; set; }
    public virtual ICollection<Parcel> Parcels { get; set; }
}

Я понимаю, как засеять простые данные (для класса PaymentSystem) и отношения «один ко многим», но какой код мне следует написать в методе Seed для создания некоторых экземпляров Parcel и BuyingItem? Я имею в виду использование DbContext.AddOrUpdate(), потому что я не хочу дублировать данные каждый раз, когда запускаю Update-Database.

protected override void Seed(ParcelDbContext context)
{
    context.AddOrUpdate(ps => ps.Id,
        new PaymentSystem { Id = 1, Name = "Visa" },
        new PaymentSystem { Id = 2, Name = "PayPal" },
        new PaymentSystem { Id = 3, Name = "Cash" });
}

protected override void Seed(Context context)
{
    base.Seed(context);

    // This will create Parcel, BuyingItems and relations only once
    context.AddOrUpdate(new Parcel() 
    { 
        Id = 1, 
        Description = "Test", 
        Items = new List<BuyingItem>
        {
            new BuyingItem() { Id = 1, Price = 10M },
            new BuyingItem() { Id = 2, Price = 20M }
        }
    });

    context.SaveChanges();
}

Этот код создает Parcel, BuyingItems и их отношения, но если мне нужно такое же BuyingItem в другом Parcel (у них есть отношение «многие ко многим»), и я повторяю этот код для второй посылки - он будет дублировать BuyingItems в базе данных (хотя Ставил такие же Ids).

Пример:

protected override void Seed(Context context)
{
    base.Seed(context);

    context.AddOrUpdate(new Parcel() 
    { 
        Id = 1, 
        Description = "Test", 
        Items = new List<BuyingItem>
        {
            new BuyingItem() { Id = 1, Price = 10M },
            new BuyingItem() { Id = 2, Price = 20M }
        }
    });

    context.AddOrUpdate(new Parcel() 
    { 
        Id = 2, 
        Description = "Test2", 
        Items = new List<BuyingItem>
        {
            new BuyingItem() { Id = 1, Price = 10M },
            new BuyingItem() { Id = 2, Price = 20M }
        }
    });

    context.SaveChanges();
}

Как я могу добавить одинаковые BuyingItem в разные Parcel?


person Dmitry Gorshkov    schedule 18.12.2011    source источник


Ответы (3)


Вы должны заполнить отношение «многие ко многим» так же, как вы строите отношение «многие ко многим» в любом коде EF:

protected override void Seed(Context context)
{
    base.Seed(context);

    // This will create Parcel, BuyingItems and relations only once
    context.AddOrUpdate(new Parcel() 
    { 
        Id = 1, 
        Description = "Test", 
        Items = new List<BuyingItem>
        {
            new BuyingItem() { Id = 1, Price = 10M },
            new BuyingItem() { Id = 2, Price = 20M }
        }
    });

    context.SaveChanges();
}

Указание Id, который будет использоваться в базе данных, имеет решающее значение, иначе каждый Update-Database будет создавать новые записи.

AddOrUpdate никоим образом не поддерживает изменение отношений, поэтому вы не можете использовать его для добавления или удаления отношений при следующей миграции. Если вам это нужно, вы должны вручную удалить отношение, загрузив Parcel с BuyingItems и вызвав Remove или Add в коллекции навигации, чтобы разорвать или добавить новое отношение.

person Ladislav Mrnka    schedule 18.12.2011
comment
Действительно ли AddOrUpdate не поддерживает изменение отношений в окончательной версии EF Migrations? Это объяснило бы проблему в этом вопросе stackoverflow.com/q/10474839/270591, не так ли? - person Slauma; 07.05.2012
comment
Это не работает должным образом, если BuyingItem.Id является столбцом идентификаторов. В том случае, когда элемент не существует, он вставляется с идентификатором, сгенерированным БД, вместо указанного вами идентификатора. Это означает, что AddOrUpdate с жестко закодированными идентификаторами не является жизнеспособным решением для заполнения, потому что он будет генерировать дубликаты при следующем запуске заполнения. Проверено на EF5. - person angularsen; 06.01.2013
comment
Ладислав, я не вижу этой работы с методом Parcel.Items.Add() для взаимосвязи связанных сущностей, как вы предложили, когда корневая сущность и связанная сущность уже существуют в БД (короче говоря, когда мы пытаемся просто обновить отношение исключительно) . При вызове Parcel.Items.Add(new BuyingItem() { Id = 1, Price = 10M }) элемент не добавляется, как можно было бы ожидать после вызова context.SaveChanges(). - person Ryan Griffith; 28.06.2014
comment
Поскольку я полагаю, что Id - это столбец идентификаторов и будет сгенерирован в dbms. В этом случае вы не можете установить желаемое значение. Таким образом, нет никакого способа быть уверенным, что не будет дубликатов, когда EF снова запустит заполнение, потому что у вас нет контроля над идентификаторами. Например, у вас есть 3 посылки (parcel1, parcel2, parcel3), и вы удалили parcel2 из раздачи. У вас будет дублированный parcel3, если вы решите снова создать свою базу данных или запустить приложение на другом экземпляре. Проблема возникает, когда идентификаторы, используемые в коде заполнения, не синхронизируются с идентификаторами в базе данных. - person Vladislav Kostenko; 13.08.2014
comment
-1, поскольку это неправильный и ошибочно принятый ответ: идентификаторы, установленные вручную, игнорируются, поэтому это не сработает и не решит проблему. У OP есть ответ с правильным решением ниже. - person Michael Sagalovich; 21.08.2015

Обновленный ответ

Убедитесь, что вы прочитали раздел «Правильное использование AddOrUpdate» ниже, чтобы получить полный ответ.

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

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<Parcel>()
        .HasMany(p => p.Items)
        .WithMany(r => r.Parcels)
        .Map(m =>
        {
            m.ToTable("ParcelItems");
            m.MapLeftKey("ParcelId");
            m.MapRightKey("BuyingItemId");
        });
}

Затем реализуйте метод Seed следующим образом:

protected override void Seed(Context context)
{
    context.Parcels.AddOrUpdate(p => p.Id,
        new Parcel { Id = 1, Description = "Parcel 1", Weight = 1.0 },
        new Parcel { Id = 2, Description = "Parcel 2", Weight = 2.0 },
        new Parcel { Id = 3, Description = "Parcel 3", Weight = 3.0 });

    context.BuyingItems.AddOrUpdate(b => b.Id,
        new BuyingItem { Id = 1, Price = 10m },
        new BuyingItem { Id = 2, Price = 20m });

    // Make sure that the above entities are created in the database
    context.SaveChanges();

    var p1 = context.Parcels.Find(1);
    // Uncomment the following line if you are not using lazy loading.
    //context.Entry(p1).Collection(p => p.Items).Load();

    var p2 = context.Parcels.Find(2);
    // Uncomment the following line if you are not using lazy loading.
    //context.Entry(p2).Collection(p => p.Items).Load();

    var i1 = context.BuyingItems.Find(1);
    var i2 = context.BuyingItems.Find(2);

    p1.Items.Add(i1);
    p1.Items.Add(i2);

    // Uncomment to test whether this fails or not, it will work, and guess what, no duplicates!!!
    //p1.Items.Add(i1);
    //p1.Items.Add(i1);
    //p1.Items.Add(i1);
    //p1.Items.Add(i1);
    //p1.Items.Add(i1);

    p2.Items.Add(i1);
    p2.Items.Add(i2);

    // The following WON'T work, since we're assigning a new collection, it'll try to insert duplicate values only to fail.
    //p1.Items = new[] { i1, i2 };
    //p2.Items = new[] { i2 };
}

Здесь мы убеждаемся, что объекты создаются / обновляются в базе данных, вызывая context.SaveChanges() в методе Seed. После этого мы получаем необходимую посылку и покупаем товарные объекты с помощью context. После этого мы используем свойство Items (которое представляет собой коллекцию) для объектов Parcel, чтобы добавить BuyingItem по своему усмотрению.

Обратите внимание: сколько бы раз мы ни вызывали метод Add, используя один и тот же объект элемента, мы не получаем нарушения первичного ключа. Это потому, что EF внутренне использует HashSet<T> для управления коллекцией Parcel.Items. HashSet<Item> по своей природе не позволяет добавлять повторяющиеся элементы.

Более того, если вам каким-то образом удастся обойти это поведение EF, как я продемонстрировал в примере, наш первичный ключ не пропустит дубликаты.

Правильное использование AddOrUpdate

При использовании типичного поля Id (int, identity) в качестве выражения идентификатора с методом AddOrUpdate следует проявлять осторожность.

В этом случае, если вы вручную удалите одну из строк из таблицы Parcel, вы в конечном итоге будете создавать дубликаты при каждом запуске метода Seed (даже с обновленным методом Seed, который я предоставил выше).

Рассмотрим следующий код,

context.Parcels.AddOrUpdate(p => p.Id,
    new Parcel { Id = 1, Description = "Parcel 1", Weight = 1.0 },
    new Parcel { Id = 2, Description = "Parcel 1", Weight = 1.0 },
    new Parcel { Id = 3, Description = "Parcel 1", Weight = 1.0 }
);

Технически (учитывая суррогатный идентификатор здесь) строки уникальны, но с точки зрения конечного пользователя они дублируются.

Истинное решение здесь - использовать поле Description в качестве выражения идентификатора. Добавьте этот атрибут к свойству Description класса Parcel, чтобы сделать его уникальным: [MaxLength(255), Index(IsUnique=true)]. Обновите следующие фрагменты в методе Seed:

context.Parcels.AddOrUpdate(p => p.Description,
    new Parcel { Description = "Parcel 1", Weight = 1.0 },
    new Parcel { Description = "Parcel 2", Weight = 2.0 },
    new Parcel { Description = "Parcel 3", Weight = 3.0 });

// Make sure that the above entities are created in the database
context.SaveChanges();

var p1 = context.Parcels.Single(p => p.Description == "Parcel 1");

Обратите внимание: я не использую поле Id, поскольку EF игнорирует его при вставке строк. И мы используем Description, чтобы получить правильный объект участка, независимо от Id значения.


Старый ответ

Я хотел бы добавить сюда несколько наблюдений:

  1. Использование Id, вероятно, не принесет никакой пользы, если столбец Id является полем, созданным базой данных. EF проигнорирует это.

  2. Кажется, что этот метод работает нормально, когда метод Seed запускается один раз. Он не будет создавать никаких дубликатов, однако, если вы запустите его во второй раз (а большинству из нас приходится делать это часто), он может внедрить дубликаты. В моем случае так и было.

Это руководство Тома Дайкстры показало мне, как это правильно делать. Это работает, потому что мы ничего не принимаем как должное. Мы не указываем ID. Вместо этого мы запрашиваем контекст по известным уникальным ключам и добавляем к ним связанные сущности (которые снова получаются путем запроса контекста). В моем случае это сработало как шарм.

person Ravi M Patel    schedule 18.08.2014
comment
Я использовал метод из упомянутого выше руководства, и он сработал. - person tekiegirl; 29.10.2014
comment
Отмеченный ответ не учитывает первичные ключи поля идентификации. Метод Seed из учебника Тома Дайкстры подходит, так что это гораздо более надежное решение. - person Brad Mathews; 11.03.2016
comment
Пожалуйста, подумайте о добавлении дополнительных деталей к вашему ответу, помимо ссылки. - person mikesigs; 11.07.2016
comment
@mikesigs Спасибо, я обновил свой ответ. Достаточно ли сейчас? - person Ravi M Patel; 11.07.2016
comment
Это определенно лучше. Однако вам не следует указывать идентификаторы в инициализаторах, и определенно не следует использовать его в качестве идентификатораExpression в вызове AddOrUpdate. Для Parcel вам лучше использовать Description (но тогда вы захотите обеспечить уникальность и для этого столбца), а для BuyingItems, ну ... там не лучший вариант. Но это проблема домена, а не EF. - person mikesigs; 11.07.2016
comment
@mikesigs, ты прав. Причина, по которой я сохранил идентификаторы нетронутыми, заключалась в том, чтобы показать, как это можно сделать, не меняя данную модель в этом случае. Я мог бы использовать поле Description в случае Parcel, но у меня не было выбора с BuyingItem. Теперь я добавил раздел, демонстрирующий правильное использование метода AddOrUpdate с модифицированным классом Parcel, имеющим уникальный ключевой индекс в поле Description, так что в одном случае его можно использовать для получения уникальных Parcel объектов. Аналогичным образом можно исправить и недоработку конструкции класса BuyingItem. - person Ravi M Patel; 11.07.2016
comment
Идеально. Так много людей не понимают подводных камней использования Id в качестве identifierExpression. Теперь, надеюсь, кто-нибудь придет и отметит это как ответ! - person mikesigs; 11.07.2016

В порядке. Я понимаю, как мне быть в той ситуации:

protected override void Seed(Context context)
{
    base.Seed(context);
    var buyingItems = new[]
    {
        new BuyingItem
        {
             Id = 1,
             Price = 10m
        },
        new BuyingItem
        {
             Id = 2,
             Price = 20m,
        }
    }

    context.AddOrUpdate(new Parcel() 
    { 
        Id = 1, 
        Description = "Test", 
        Items = new List<BuyingItem>
        {
            buyingItems[0],
            buyingItems[1]
        }
    },
    new Parcel() 
    { 
        Id = 2, 
        Description = "Test2", 
        Items = new List<BuyingItem>
        {
            buyingItems[0],
            buyingItems[1]
        }
    });

    context.SaveChanges();
}

В базе нет дубликатов.

Спасибо, Ладислав, ты дал мне верный вектор для решения моей задачи.

person Dmitry Gorshkov    schedule 18.12.2011