Entity Framework one-to-many с табличной иерархией создает один столбец внешнего ключа для каждого подкласса

У меня есть Garage, который содержит Cars и Motorcycles. Автомобили и мотоциклы Vehicles. Вот они:

public class Garage
{
    public int Id { get; set; }
    public virtual List<Car> Cars { get; set; }
    public virtual List<Motorcycle> Motorcycles { get; set; }

    public Garage()
    {
        Cars = new List<Car>();
        Motorcycles = new List<Motorcycle>();
    }
}

public abstract class Vehicle
{
    public int Id { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }
}

public class Car : Vehicle
{
    public int GarageId { get; set; }
    public virtual Garage Garage { get; set; }
    // some more properties here...
}

public class Motorcycle : Vehicle
{
    public int GarageId { get; set; }
    public virtual Garage Garage { get; set; }
    // some more properties here...
}

Почему у автомобилей и мотоциклов есть свойство GarageId и Garage? Если я подталкиваю эти свойства до суперкласса Vehicle, EF жалуется и говорит, что свойства навигации должны находиться в конкретных классах.

Перемещение, здесь мой DbContext:

public class DataContext : DbContext
{
    public DbSet<Garage> Garages { get; set; }
    public DbSet<Vehicle> Vehicles { get; set; }
    public DbSet<Car> Cars { get; set; }
    public DbSet<Motorcycle> Motorcycles { get; set; }

    public DataContext()
        : base("GarageExample")
    {

    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
        modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
    }
}

И вот короткая программа для игры с моими игрушками:

class Program
{
    static void Main(string[] args)
    {
        Database.SetInitializer<DataContext>(new DropCreateDatabaseAlways<DataContext>());

        using (var db = new DataContext())
        {
            var car1 = new Car { Make = "Subaru", Model = "Legacy" };
            var car2 = new Car { Make = "Porche", Model = "911" };

            var bike1 = new Motorcycle { Make = "Suzuki", Model = "GS500" };
            var bike2 = new Motorcycle { Make = "Kawasaki", Model = "Ninja" };

            var garage = new Garage();

            garage.Cars.Add(car1);
            garage.Cars.Add(car2);
            garage.Motorcycles.Add(bike1);
            garage.Motorcycles.Add(bike2);

            db.Garages.Add(garage);

            db.SaveChanges();
        }
    }
}

Программа запускается и создает следующую таблицу Транспортные средства:

Id Make     Model  GarageId GarageId1 Discriminator
1  Subaru   Legacy 1        null      Car
2  Porche   911    1        null      Car
3  Suzuki   GS500  null     1         Motorcycle
4  Kawasaki Ninja  null     1         Motorcycle

Как у автомобилей, так и у мотоциклов, обладающих собственными свойствами GarageId и Garage, кажется, что каждый подкласс создает свой собственный внешний ключ для гаража. Как я могу сказать EF (с помощью свободного Api, если возможно), что Car.Garage и Motorcycle.Garage - это одно и то же, и должен использовать тот же столбец?

Это Транспортные средства, которые я хочу, конечно:

Id Make     Model  GarageId Discriminator
1  Subaru   Legacy 1        Car
2  Porche   911    1        Car
3  Suzuki   GS500  1        Motorcycle
4  Kawasaki Ninja  1        Motorcycle

Ответ 1

Используйте атрибут [Column ( "GarageId" )] для свойства GarageId как для класса автомобиля, так и для мотоцикла.

Ответ 2

Единственный способ, которым я знаю, получить один столбец внешнего ключа и схему базы данных, которую вы хотите, - отказаться от коллекций навигации для производного типа в Garage и вместо этого использовать одну коллекцию для базового типа:

public class Garage
{
    public int Id { get; set; }
    public virtual List<Vehicle> Vehicles { get; set; }

    public Garage()
    {
        Vehicles = new List<Vehicle>();
    }
}

public abstract class Vehicle
{
    public int Id { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }

    public int GarageId { get; set; }
    public virtual Garage Garage { get; set; }
}

public class Car : Vehicle
{
    // some more properties here...
}

public class Motorcycle : Vehicle
{
    // some more properties here...
}

Конечно, вы теряете фильтр удобного типа с ленивой или нетерпеливой загрузкой, когда хотите загружать Car или Motorcycle из Garage, и вам нужно либо загрузить все Vehicle в Garage или использовать проекции или явную загрузку для загрузки производных типов.

По-моему, это совершенно верно, что вы пытаетесь сделать, но каким-то образом оно не поддерживается с помощью Entity Framework, или сопоставление с столбцами FK не было реализовано таким образом, что этот сценарий может поддерживаться.

Ответ 3

    public class Garage
    {
        public int Id { get; set; }
        public virtual List<Car> Cars { get; set; }
        public virtual List<Motorcycle> Motorcycles { get; set; }

        public Garage()
        {
            Cars = new List<Car>();
            Motorcycles = new List<Motorcycle>();
        }
    }

    public abstract class Vehicle
    {
        public int Id { get; set; }
        public int GarageId { get; set; }
        public string Make { get; set; }
        public string Model { get; set; }
    }

    public class Car : Vehicle
    {
        [ForeignKey("GarageId")]
        public virtual Garage Garage { get; set; }
        // some more properties here...
    }

    public class Motorcycle : Vehicle
    {
        [ForeignKey("GarageId")]
        public virtual Garage Garage { get; set; }
        // some more properties here...
    }

Ответ 4

Вы еще посмотрели на это?

Отображение наследования на основе иерархии таблиц (TPH)

В сценарии сопоставления TPH все типы в иерархии наследования сопоставлены с одной таблицей. Столбец дискриминатора используется для идентификации тип каждой строки. При создании вашей модели с помощью Code First TPH стратегия по умолчанию для типов, которые участвуют в наследовании иерархия. По умолчанию столбец дискриминатора добавляется в таблицу с именем "Дискриминатор" и именем типа CLR каждого типа в иерархия используется для значений дискриминатора. Вы можете изменить по умолчанию с использованием свободного API.

modelBuilder.Entity<Course>() 
.Map<Course>(m => m.Requires("Type").HasValue("Course")) 
.Map<OnsiteCourse>(m => m.Requires("Type").HasValue("OnsiteCourse"));

Прямо от здесь.