Я пытаюсь создать систему заказов. Базовым объектом является Sku, который представляет некоторый артикул:
public class Sku
{
[Key]
public Guid Id { get; set; }
//a lot of Sku information here
}
Также у меня есть сущности Customer и OrderPicker
public class Customer
{
[Key]
public Guid Id { get; set; }
// some Customer info here
public List<Order>? Orders { get; set; }
}
public class OrderPicker
{
[Key]
public Guid Id { get; set; }
//some picker data here
public List<Order>? Orders { get; set; }
}
Затем я хочу создать объект, который представляет сам заказ. Очевидно, что у заказа есть только один клиент, а у клиента много заказов. Насколько я понимаю, эту связь можно смоделировать с помощью следующего кода:
public class Order
{
[Key]
public Guid Id { get; set; }
[Required]
public Customer Customer { get; set; }
public Guid CustomerGuid { get; set; }
//order data
}
И в AppContext:
modelBuilder.Entity<Order>()
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerGuid)
.HasPrincipalKey(c => c.Id);
То же самое относится и к отношению «заказ-сборщик».
Проблемы возникают, когда я пытаюсь смоделировать отношения между заказом и товаром. Предположим, что заказ должен содержать много Sku в паре с их количеством в этом заказе. Мне пришла в голову идея ввести сущность коннектора OrderItem, которая содержит следующие поля:
[Owned]
public class OrderItem
{
public Guid SkuId { get; set; }
public Sku Sku { get; set; }
public Order Order { get; set; }
public uint Quantity { get; set; }
}
Тогда в Order я могу объявить ICollection<OrderItems> и работать с ними:
public class Order
{
[Key]
public Guid Id { get; set; }
[Required]
public Customer Customer { get; set; }
public Guid CustomerGuid { get; set; }
public ICollection<OrderItem> OrderItems { get; set; }
//order data
}
На этом этапе я не уверен, как я могу указать FK и PK для этого объекта и действительно ли они ему нужны.
Тогда мне нужно держать запас для SKUS. Итак, я создал объект StockItem:
public class StockItem
{
public Sku Sku { get; private set; }
[Key]
public Guid SkuId { get; private set; }
[Required]
public uint StockBalance { get; private set; }
}
Вышеупомянутое не работает, при попытке добавить первоначальную миграцию выдаются разные ошибки. Все ошибки связаны с StockItem и его связью с Sku. Например:
Свойство "Sku" "StockItem.Sku" не может быть сопоставлено, поскольку поставщик базы данных не поддерживает этот тип. Рассмотрите возможность преобразования значения свойства в тип, поддерживаемый базой данных, с помощью преобразователя значений.
Итак, у меня есть 2 вопроса:
OrderItem и StockItem?




Одной из проблем может быть использование SkuId для PK StockItem. Похоже, что Sku по сути похож на продукт? OrderItem может иметь собственный PK, например OrderItemId, ссылающийся на Sku. Если вы хотите отслеживать уровни запасов (баланс) для Skus, вы можете создать отдельную таблицу/сущность для StockItem/StockLevel, которая может использовать SkuId в качестве PK и FK для Sku.
Набросок для размышления:
public class Order
{
[Key]
public Guid Id { get; set; }
[Required]
[ForeignKey("CustomerId")]
public virtual Customer Customer { get; set; }
//order data
public ICollection<OrderItem> OrderItems { get; private set; } = new List<OrderItem>();
}
public class OrderItem
{
[Key]
public Guid Id { get; set; }
// order item details. such as Quantity.
[ForeignKey("OrderId")]
public virtual Order Order { get; set; }
[ForeignKey("SkuId")]
public virtual Sku Sku { get; set; }
}
public class Sku
{
[Key]
public Guid Id { get; set; }
// SKU (product?) fields.
}
public class StockLevel
{
[Key, ForeignKey(nameof(Sku))]
public Guid Id { get; set; }
public uint StockBalance { get; set; }
public virtual Sku Sku { get; set; }
}
При отображении отношений у меня есть одно предложение: не объявлять свойства FK, а использовать теневые свойства. Это устраняет проблему необходимости поддерживать два источника достоверности, которые могут время от времени различаться и приводить к проблемам поведения в зависимости от того, какой из них вы используете, когда свойство навигации загружено или нет. Отношение между заказом и элементом заказа - один ко многим. В одном заказе много позиций заказа. Свойство обратной навигации (OrderItem.Order) является необязательным, и если оно не нужно, я бы просто удалил его и ссылался на элементы заказа исключительно через совокупный корень заказа. Отношения между OrderItem и Sku являются многими-к-одному. Многие позиции заказа могут относиться к одному и тому же артикулу. Order Item будет иметь Sku ID FK, Sku, как правило, не должен иметь коллекцию OrderItems. Связь для Sku и StockLevel будет один к одному, где SKU является основным в связи, где один StockLevel может ссылаться на него, используя один и тот же PK. Обратите внимание, что в StockLevel столбец Id действует как PK и FK для Sku.
Другое предложение, когда речь идет о коллекциях в сущностях, — всегда инициализировать их. Либо в строке, как в примере выше, либо в конструкторе. С сущностями вы хотите избежать повторной инициализации каким-либо внешним кодом набора связанных сущностей, поэтому Setter делается закрытым. Если бы какой-либо код установил order.OrderItems = new List<OrderItem>();, это довольно легко привело бы к проблемам.
Мы надеемся, что это должно дать вам некоторые идеи по настройке отношений, но не стесняйтесь расширять/комментировать любые конкретные вопросы или сценарии, с которыми у вас все еще есть проблемы.
Также я настроил OrderItem как собственный тип, потому что мне не нужно иметь DbSet<OrderItem> в моем AppContext. Вы удалили эту аннотацию. Могу ли я оставить его на месте? И если можно, то лучше оставить или удалить?
Вы упомянули, что лучше избегать присваивания внешних значений связанным сущностям. Но что, если система предназначена для такого поведения? Это действительно проблема дизайна или просто момент, о котором нужно знать? Например, у заказа есть один клиент, который уже известен при инициализации заказа, но мы не знаем, кто будет собирать этот заказ, потому что это зависит от того, какой сборщик освободится первым.
Только когда дело доходит до повторной инициализации коллекций. Если у вас есть что-то подобное, и у него есть ссылка на средство выбора, это обычно будет ссылка с нулевым значением, которая будет установлена позже и, возможно, изменится с одного средства выбора/пользователя на другого. Это прекрасно. Только с коллекциями может показаться, что быстрее/проще изменить коллекцию, установив ее в новый список, содержащий новый набор элементов, а не удаляя и добавляя изменения по отдельности. Это то, чего следует избегать, поэтому хорошо инициализировать коллекции один раз и сделать установщик закрытым.
В некоторых гайдах я видел примеры, когда в связях один-ко-многим авторы указывают ICollection<EntityB> в EntityA и указывают только EntityAId в EntityB без указания свойства навигации public virtual EntityA и без каких-либо аннотаций. Можно ли применить такой подход к моему случаю? Если да, то как EF понимает, что EntityAId является ключом отношения?
Это можно сделать даже без EntityAId, объявленного в EntityB через теневые свойства. EF может управлять FK по соглашению, если имя FK соответствует связанному типу. Отношение будет отображаться явно в OnModelCreating или с использованием конфигурации типа сущности, поэтому, например, modelBuilder.Entity<EntityA>().HasMany(x => x.EntityBs).WithOne(); должно работать, если таблица EntityB имеет EntityAId в качестве обратной стороны FK. Если у вас было другое имя FK, например «ParentId», дополните его: .WithOne().ForeignKey("ParentId");
Вау, это суперинформативно, спасибо!
Я много раз видел
virtualключевое слово в документах, но не объясняется, почему мы должны отмечать свойстваvirtual. Так будем? Если должны, то почему?