Я столкнулся с неожиданным поведением Entity Framework Core при создании миграции для иерархии классов Person. В частности, когда я генерирую миграцию для класса Person, я получаю внешний ключ с именем Student_InstitutionId вместо InstitutionId в таблице Person.
public abstract class Person : Aggregate<long>
{
// ... properties ...
}
public class Student : Person
{
// ... properties ...
public long InstitutionId { get; private set; }
public virtual Institution Institution { get; set; }
}
public class Institution : Aggregate<long>
{
// ... properties ...
public virtual ICollection<Student> Students { get; set; }
}
Мои конфигурации Fluent API следующие:
public class PersonEntityConfig : BaseEntityConfig<Person, long>
{
public override void Configure(EntityTypeBuilder<Person> builder)
{
// ... configuration ...
}
}
public class StudentEntityConfig : BaseEntityConfig<Student, long>
{
public override void Configure(EntityTypeBuilder<Student> builder)
{
// ... configuration ...
builder.HasOne(a => a.Institution)
.WithMany(a => a.Students)
.HasForeignKey(a => a.InstitutionId)
.OnDelete(DeleteBehavior.NoAction);
}
}
public class InstitutionEntityConfig : BaseEntityConfig<Institution, long>
{
#region Fields
#endregion
#region Properties
#endregion
#region Methods
public override void Configure(EntityTypeBuilder<Institution> builder)
{
base.Configure(builder);
builder.Property(x => x.Name).HasMaxLength(50).IsRequired();
builder.Property(x => x.CenterNumber).HasMaxLength(50);
builder.Property(x => x.EMISNumber).HasMaxLength(50);
builder.Property(x => x.EmailAddress).HasMaxLength(50);
builder.HasOne(a => a.Address)
.WithOne(i => i.Institution)
.HasForeignKey<Institution>(x => x.AddressId)
.OnDelete(DeleteBehavior.Cascade);
}
#endregion
#region Constructors
#endregion
}
Похоже, проблема связана с тем, как Entity Framework Core обрабатывает свойство дискриминатора и внешние ключи в таблице Person. Как я могу гарантировать, что внешний ключ правильно назван InstitutionId вместо Student_InstitutionId?
Я ожидаю, что внешний ключ в таблице Person будет называться InstitutionId, а не Student_InstitutionId. При создании миграции Entity Framework Core создает внешний ключ с именем Student_InstitutionId в таблице Person.
Предоставьте инструкции, как решить эту проблему с именованием и убедиться, что внешний ключ правильно назван InstitutionId.
Все свойства, сопоставленные со столбцом, должны иметь общедоступные методы получения и установки. Так что сделайте свой сеттер общедоступным.
Как объяснено в ссылке выше, это связано с соглашениями об именах EF. Вам нужно будет установить имя столбца, используя метод HasColumnName
или HasConstraintName
в классе конфигурации.
Похоже, вы используете наследование TPH. Имеют ли какие-либо другие типы, наследующие от Person, также свойство навигации Institution?
@StevePy - Да, есть
Мне нужно будет провести пару тестов, но я подозреваю, что вы не сможете повторно использовать один и тот же столбец FK для нескольких типов наследования TPH. Если бы Person содержал свойство навигации, которое было общим для всех подклассов, тогда это было бы нормально, но если 2/3 подклассов имеют «Учреждение», а третий — нет, EF может потребовать уникальные свойства FK с нулевым значением для Person для каждого. (Или он может поддерживать сопоставление с тем же столбцом, но для этого существует ошибка/ограничение в коде). Использование TPT или TPC не будет проблемой для настройки этого сценария, но потенциально является ограничением TPH.
После тестирования нет, это не будет работать «из коробки» со структурой наследования TPH, «сначала код» или «сначала схема», если некоторые из наследующих классов имеют отношения, а другие — нет. Используя следующий пример:
[Table("Animals")]
public abstract class Animal
{
[Key]
public int AnimalId { get; set; }
public string Name { get; set; }
}
public class Dog : Animal
{
public virtual Owner Owner { get; set; }
}
public class Cat : Animal
{
public virtual Owner Owner { get; set; }
}
public class Wolf : Animal
{
}
с конфигурацией:
modelBuilder.Entity<Animal>()
.HasDiscriminator<string>("AnimalType")
.HasValue<Dog>("Dog")
.HasValue<Cat>("Cat")
.HasValue<Wolf>("Wolf");
modelBuilder.Entity<Dog>()
.HasOne(x => x.Owner)
.WithMany()
.IsRequired(false)
.HasForeignKey("OwnerId")
.HasConstraintName("FK_Dogs_Owners");
modelBuilder.Entity<Cat>()
.HasOne(x => x.Owner)
.WithMany()
.HasForeignKey("OwnerId")
.HasConstraintName("FK_Cats_Owners");
Даже попытка «обмануть» его с помощью отдельных ограничений FK не сработала, если мы хотим, чтобы у Cats & Dogs был владелец, а не у Wolves. Он ожидает Dog_OwnerId и Cat_OwnerId в таблице Animal. Если у всех животных есть владелец, это нормально, мы можем указать «Владелец» в «Животное», и все будет работать, но тогда у волков будут владельцы. В любом случае, независимо от того, используем ли мы один OwnerId или DogOwnerId/CatOwnerId, эти FK должны иметь возможность иметь нулевое значение, что может быть не идеальным с точки зрения ссылочной целостности, если ожидается, что Cats и Dogs потребуют владельца. При использовании TPH нам понадобятся DogOwnerId и CatOwnerId в таблице Animal, иначе мы можем слегка «взломать» ее:
[Table("Animals")]
public abstract class Animal
{
[Key]
public int AnimalId { get; set; }
public string Name { get; set; }
public virtual Owner? Owner { get; set; }
}
public class Dog : Animal
{
}
public class Cat : Animal
{
}
public class Wolf : Animal
{
public new virtual Owner? Owner { get { return null; }; set { if (value != null) throw new InvalidOperationException("Wolves have no owner."); } }
}
Это перемещает Владельца в Animal, чтобы поддерживать ассоциацию FK, но мы перегружаем Владельца в Wolf, чтобы, если что-то попытается установить владельца, мы выдали исключение. Это не мешает базе данных назначать волку владельца, и EF с радостью даже запросит волков по владельцу.
Лучшим решением, если некоторые унаследованные классы имеют отношения, а другие нет, и чтобы правильно обеспечить ссылочную целостность, было бы переключиться на наследование TPT. Здесь из таблицы Animal мы удалили OwnerId и Дискриминатор. Мы добавляем таблицу Dog с AnimalId как PK + FK для Animal и OwnerId FK для владельца. Мы добавляем таблицу Cat с AnimalId как PK + FK для Animal и OwnerId FK для владельца. Мы добавляем таблицу Wolf с Animal как PK + FK в Animal, без OwnerId. Сущности могут быть немного обновлены для их имен таблиц:
[Table("Animals")]
public abstract class Animal
{
[Key]
public int AnimalId { get; set; }
public string Name { get; set; }
}
[Table("Dogs")]
public class Dog : Animal
{
public virtual Owner Owner { get; set; }
}
[Table("Cats")]
public class Cat : Animal
{
public virtual Owner Owner { get; set; }
}
[Table("Wolves")]
public class Wolf : Animal
{
}
... и отображение:
modelBuilder.Entity<Animal>()
.ToTable("Animals")
modelBuilder.Entity<Dog>()
.HasOne(x => x.Owner)
.WithMany()
.HasForeignKey("OwnerId");
modelBuilder.Entity<Cat>()
.HasOne(x => x.Owner)
.WithMany()
.HasForeignKey("OwnerId");
Загвоздка здесь в том, что мы можем использовать атрибут [Table("Dogs")]
для таблиц подклассов, но нам все равно нужен явный атрибут .ToTable("Animals")
для базового класса. Похоже, что это требование для признания ТРТ. Похоже, что простое использование атрибута [Table]
заставляет EF использовать наследование TPC.
Можете ли вы заставить имена столбцов быть одинаковыми? .Property("OwnerId").HasColumnName("OwnerId")
Нет, хотя в конфигурации им явно задано одно и то же значение, когда EF приступает к реализации отношений, он этого не позволяет и требует явно уникальных свойств/столбцов. Это можно считать ошибкой в EF или недопустимым для некоторых реализаций базы данных. Кажется, это работает для SQL Server, у меня может быть два FK, указывающих от Animal.OwnerId до Owner. Это ничего не значит, поскольку они имеют одно и то же значение, если все, что нужно, — это уникальный FK. Если между подклассами есть какие-либо существенные различия, я рекомендую TPC или TPT вместо TPH.