Как посеять данные с отношениями "многие-к-связи" в миграции 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 (они имеют отношение многие ко многим), и я повторяю этот код для второго parcel - он будет дублировать BuyingItems в базе данных (хотя я установил те же Id s).

Пример:

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?

Ответы

Ответ 1

Вы должны заполнить отношение "многие ко многим" так же, как вы создаете отношение "многие ко многим" в любом коде 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 в коллекции навигации, чтобы разбить или добавить новое отношение.

Ответ 2

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

Убедитесь, что вы читаете раздел "Использование 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, используя один и тот же объект item, мы не получим нарушение первичного ключа. Это связано с тем, что 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 запускается один раз. Он не будет создавать дубликаты, однако, если вы запустите его во второй раз (и большинство из нас должны делать это часто), он может ввести дубликаты. В моем случае это так.

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

Ответ 3

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

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();
}

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

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