Страницы

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

понедельник, 6 января 2020 г.

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

#c_sharp #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 и более внешних
ключа, указывающих на одну и ту же таблицу.
    


Ответы

Ответ 1



Ваша проблема - в соответствии между навигационными свойствами. Свойства, ведущие в две стороны, идут парами. Вы разбиение на пары не указывали - отсюда и странности. Ваша первая попытка сделала два свойства - 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(); } Теперь сама СУБД будет отслеживать, что пол родителя соответствует его роли, а родители каждой лошади - разнополые.

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

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