Страницы

Поиск по вопросам

среда, 13 февраля 2019 г.

Entity Framework. таблица ссылающаяся на себя

Коллеги, помогите пожалуйста.
EF, Code First. Есть таблица, в которой я хочу хранить генеалогическую информацию о лошадях (ссылки на мать и отца, если они известны). Насколько я это себе представляю, каждая запись таблицы может содержать от 0 до 2 внешних ключей, ссылающихся на первичный ключ другой записи этой же таблицы.
1. Для меня проще использовать DataAnnotations, поэтому начал я так
public class Horse { public int Id { get; set; }
[ForeignKey("Father")] public int? FatherId { get; set; } public virtual Horse Father { get; set; }
[ForeignKey("Mother")] public int? MotherId { get; set; } public virtual Horse Mother { get; set; } }
Во время создания миграции получаю ошибку. Horse_Mother_Target: : Multiplicity is not valid in Role 'Horse_Mother_Target' in relationship 'Horse_Mother'. Because the Dependent Role properties are not the key properties, the upper bound of the multiplicity of the Dependent Role must be '*'.. Тут я не понял проблемы, поясните пожалуйста что не так?
2. Добавляю к классу св-во Children, описывающее вторую сторону связи.
public class Horse { public int Id { get; set; }
[ForeignKey("Father")] public int? FatherId { get; set; } public virtual Horse Father { get; set; }
[ForeignKey("Mother")] public int? MotherId { get; set; } public virtual Horse Mother { get; set; }
public virtual ICollection Children { get; set; } }
Миграция создается без ошибок, но выглядит так:
AddColumn("dbo.Horses", "Horse_Id", c => c.Int()); CreateIndex("dbo.Horses", "FatherId"); CreateIndex("dbo.Horses", "MotherId"); CreateIndex("dbo.Horses", "Horse_Id"); AddForeignKey("dbo.Horses", "Horse_Id", "dbo.Horses", "Id"); AddForeignKey("dbo.Horses", "FatherId", "dbo.Horses", "Id"); AddForeignKey("dbo.Horses", "MotherId", "dbo.Horses", "Id");
Два требуемых внешних ключа создались, смотрят куда надо, однако что это за столбец Horse_Idи почему он автоматически генерируется, непонятно...
3. Решил попробовать FluentApi. Убрал DataAnnotations в классе, написал такое:
modelBuilder.Entity() .HasOptional(h => h.Father) .WithMany(x => x.Children) .HasForeignKey(x => x.FatherId) .WillCascadeOnDelete(false);
modelBuilder.Entity() .HasOptional(h => h.Mother) .WithMany(x => x.Children) .HasForeignKey(x => x.MotherId) .WillCascadeOnDelete(false);
По смыслу похоже на правду. По итогу получил следующую ошибку: Sequence contains no matching element. Тут вообще никаких мыслей нет, что произошло...
4. Решил попробовать создать только 1 внешний ключ. Убрал кусок, связанный с MotherId (DataAnnotations выкинул еще до этого), оставил только
modelBuilder.Entity() .HasOptional(h => h.Father) .WithMany(x => x.Children) .HasForeignKey(x => x.FatherId)
И неожиданно получил миграцию, которую хотел увидеть изначально:
CreateIndex("dbo.Horses", "FatherId"); CreateIndex("dbo.Horses", "MotherId"); AddForeignKey("dbo.Horses", "FatherId", "dbo.Horses", "Id"); AddForeignKey("dbo.Horses", "MotherId", "dbo.Horses", "Id");
После таких приключений я окончательно запутался. Прошу, поясните по пунктам, где именно я некорректно прописывал требования к EF и как это надо было делать правильно. Как следует конфигурировать связи в общем случае, когда в одной таблице 2 и более внешних ключа, указывающих на одну и ту же таблицу.


Ответ

Ваша проблема - в соответствии между навигационными свойствами. Свойства, ведущие в две стороны, идут парами. Вы разбиение на пары не указывали - отсюда и странности.
Ваша первая попытка сделала два свойства - Father и Mother - встречными друг другу (они стали такими автоматически). Разумеется, это глупо и даже сама EF это поняла, о чем и сказала, пусть и в такой странной манере:
Horse <-> Horse ----------------- Father <-> Mother
Во втором варианте EF не смогла объединить свойства в пары - и они стали независимыми:
Horse <-> Horse ------------------- Father -> Mother -> <- Children
В третьем варианте вы попробовали сопоставить свойство Children два раза. EF так не умеет:
Horse <-> Horse ------------------- Father <-> Children Mother <-> Children
В четвертом варианте вам только кажется что все правильно. На самом деле вы сопоставили Father и Children, а Mother осталась без пары:
Horse <-> Horse ------------------- Father <-> Children Mother ->

Теперь как делать правильно. Способ первый. Отказываемся от общего свойства Children:
class Horse { // ...
public virtual Horse Father { get; set; } public virtual Horse Mother { get; set; } [InverseProperty("Father")] public virtual ICollection FatherChildren { get; set; } [InverseProperty("Mother")] public virtual ICollection MotherChildren { get; set; } }
Более красивый способ - использовать отдельный класс-связку:
class Horse { // ...
public virtual ICollection Parents { get; set; } public virtual ICollection Children { get; set; } }
class HorseParent { public int ParentId { get; set; }
[Key, Column(Order = 0)] public int ChildId { get; set; }
[Key, Column(Order = 1)] public bool IsFather { get; set; }
[ForeignKey("ParentId"), InverseProperty("Children")] public virtual Horse Parent { get; set; }
[ForeignKey("ChildId"), InverseProperty("Parents")] public virtual Horse Child { get; set; } }
Здесь я использовал составной первичный ключ для того, чтобы ограничить число родителей.
PS если у вас возникает проблема с каскадным удалением - уберите конвенцию OneToManyCascadeDeleteConvention из ModelBuilder или переходите c DataAnnotations на FluentApi.

Но это еще не идеал. Дело в том, что в текущем виде лошадь может быть одновременно отцом для одного ребенка и матерью - для другого. Этого можно избежать путем включения пола в первичный ключ:
class Horse { [Key, Column(Order = 0)] public int Id { get; set; }
[Key, Column(Order = 1)] public bool IsMale { get; set; }
public virtual ICollection Parents { get; set; } public virtual ICollection Children { get; set; } }
class HorseParent { [Key, Column(Order = 0)] public int ChildId { get; set; } [Column(Order = 1)] public bool IsChildMale { get; set; } // Не нужно семантически, но нужно для внешнего ключа
[Column(Order = 2)] public int ParentId { get; set; } [Key, Column(Order = 3)] public bool IsFather { get; set; }
public virtual Horse Parent { get; set; } public virtual Horse Child { get; set; } }
protected override void OnModelCreating(DbModelBuilder b) { b.Conventions.Remove();
b.Entity().HasMany(h => h.Parents).WithRequired(h => h.Child) .HasForeignKey(p => new { p.ChildId, p.IsChildMale }); b.Entity().HasMany(h => h.Children).WithRequired(h => h.Parent) .HasForeignKey(p => new { p.ParentId, p.IsFather });
b.Entity(); }
Теперь сама СУБД будет отслеживать, что пол родителя соответствует его роли, а родители каждой лошади - разнополые.

Комментариев нет:

Отправить комментарий