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