Здравствуйте ,изучая материалы по Entity Framework прочитал о возможностях и разных
подходах при реализации наследования в данном фреймворке . И меня конечно же заинтересовал
такой вопрос :
Какая разница между их использованием и реализацией при разработке?
Какие же пункты я буду освещать при формировании ответа :
Теория - здесь я буду рассказывать какие собственно есть подходы и как они отличаются
в плане реализации
Практика - здесь я буду показывать саму реализацию кода модели классов + покажу как
будет выглядеть таблица/таблицы баз данных при разных подходах
Характеристическое сравнение - в заключительном участке мы с вами выясним какой же
подход ,в какой категории и почему именно этому подходу дано предпочтение .
Значение данной статьи : пока изучал ,собственно, эти же подходы ,то в голову сразу
пришла идея создать пособие где каждый может наглядно увидеть разницу при их использовании
и на основе характеристик и практических примеров увидеть и решить для себя какой именно
подход будет оптимальным
Ответы
Ответ 1
Итак ,начнем пожалуй .
Теоретическая часть :
Всего в Entity Framework есть всего 3 подхода :
TPH (Table Per Hierarchy - Таблица на одну иерархию классов)
TPT (Table Per Type - Таблица на тип)
TPC (Table Per Class(Concrete Type) - Таблица на каждый отдельный тип/класс)
1) TPH (Table Per Hierarchy - Таблица на одну иерархию классов) - При использовании
данного подхода - для одной иерархии классов используется одна таблица.
Данные базовых и производных классов сохраняются в одну таблицу, а для их отличия
создается специальный столбец.
2) TPT (Table Per Type - Таблица на тип) - данный подход предполагает сохранение
в общей таблице только тех свойств, которые общие для всех классом-наследников, то
есть которые определены в базовом классе.
А те свойства, которые относятся только к производному классу, сохраняются в отдельной
таблице.
3) TPC (Table Per Class(Concrete Type) - Таблица на каждый отдельный тип/класс) -
предполагает создание для каждой модели по отдельной таблицы. Столбцы в каждой таблице
создаются по всем свойствам, в том числе и унаследованным.
Здесь мы с теоретической частью пожалуй закончим - перейдем до практики:
Практическая часть:
Я считаю ,что надо выяснить одну деталь сразу :
Суть и предназначение у этих подходов одна - отобразить зависимость класса-наследника
от класса-родителя и разность заключается в том ,что я буду показывать какой именно
подход будет оптимальней с разных характеристических сторон(ну и само собою - отличия
при их практической реализации). Спасибо за внимание!
1) Подход TPH :
Итак у нас есть базовая мини-иерархия двух классов :
public class Phone
{
public int Id { get; set; }
public string Name { get; set; }
public string Company { get; set; }
public int Price { get; set; }
}
public class Smartphone : Phone
{
public string OS { get; set; }
}
class MobileContext : DbContext
{
public MobileContext() : base("DefaultConnection")
{ }
public DbSet Phones { get; set; }
public DbSet Smarts { get; set; }
}
Здесь класс Smartphone наследуется от Phone, определяя одно свойство в дополнение
к унаследованным.
И при работе будет создана такая таблица:
Кроме всех свойств классов Phone и Smartphone здесь также появляется еще один столбец
- Discriminator. Он имеет тип nvarchar и имеет длину в 128 символов. Данный столбец
и будет определять относится строка к типу Phone или Smartphone.
Работа в программе :
using(MobileContext db = new MobileContext())
{
db.Phones.Add(new Phone {Name = "Samsung Galaxy S5", Company = "Samsung", Price
= 14000 });
db.Phones.Add(new Phone {Name = "Nokia Lumia 630", Company = "Nokia", Price =
8000 });
Smartphone s1 = new Smartphone { Name = "iPhone 6", Company = "Apple", Price
= 32000, OS = "iOS" };
db.Smarts.Add(s1);
db.SaveChanges();
foreach (Phone p in db.Phones)
Console.WriteLine("{0} ({1}) - {2}", p.Name, p.Company, p.Price);
Console.WriteLine();
foreach (Smartphone p in db.Smarts)
Console.WriteLine("{0} ({1}, {2}) - {3}", p.Name, p.Company, p.Price, p.OS);
}
Прошу вас обратить внимание на одну деталь : при выводе данных из Phones также будет
идти вывод экземпляра Smartphone вместе с остальными экземплярами ,поскольку здесь
SmartPhone и есть объектом Phone.
2) Подход TPT :
Чтобы применить подход, возьмем из предыдущего примера систему классов и добавим
к классу Smartphone атрибут [Table]:
public class Phone
{
public int Id { get; set; }
public string Name { get; set; }
public string Company { get; set; }
public int Price { get; set; }
}
[Table("Smartphones")]
public class Smartphone : Phone
{
public string OS { get; set; }
}
class MobileContext : DbContext
{
public MobileContext() : base("DefaultConnection")
{ }
public DbSet Phones { get; set; }
public DbSet Smarts { get; set; }
}
Все остальное остается также, как и при подходе TPH. Но теперь база данных будет
содержать следующие таблицы:
Таблица для смартфонов содержит только одно поле OS, а также ключ Id для связи с
таблицей Phones.
Применение моделей будет аналогично подходу TPH(пункт выше):
using(MobileContext db = new MobileContext())
{
db.Phones.Add(new Phone {Name = "Samsung Galaxy S5", Company = "Samsung", Price
= 14000 });
db.Phones.Add(new Phone {Name = "Nokia Lumia 630", Company = "Nokia", Price =
8000 });
Smartphone s1 = new Smartphone { Name = "iPhone 6", Company = "Apple", Price
= 32000, OS = "iOS" };
db.Smarts.Add(s1);
db.SaveChanges();
foreach (Phone p in db.Phones)
Console.WriteLine("{0} ({1}) - {2}", p.Name, p.Company, p.Price);
Console.WriteLine();
foreach (Smartphone p in db.Smarts)
Console.WriteLine("{0} ({1}, {2}) - {3}", p.Name, p.Company, p.Price, p.OS);
}
3) Подход TPC :
Чтобы применить подход, изменим объявления моделей и контекст следующим образом:
public class Phone
{
[Key, DatabaseGenerated (DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string Name { get; set; }
public string Company { get; set; }
public int Price { get; set; }
}
public class Smartphone : Phone
{
public string OS { get; set; }
}
class MobileContext : DbContext
{
public MobileContext() : base("DefaultConnection")
{ }
public DbSet Phones { get; set; }
public DbSet Smarts { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity()
.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Phones");
});
modelBuilder.Entity().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Smarts");
});
}
}
Во-первых, обратите внимание, что у класса Phone в качестве типа ключа используется
не int, а Guid. Это поможет нам избежать некоторых проблем с ключами. Хотя также можно
было бы использовать int с ручной установкой Id при создании объекта.
Во-вторых, при настройке сопоставления моделей и таблиц у каждой модели вызывается
метод MapInheritedProperties(), который указывает Entity Framework-у, что в таблицу
для данной модели надо включить также наследуемые свойства, а не только те, которые
определены непосредственно в этой модели.
При генерации базы данных у нас будут созданы две таблицы с полным набором столбцов:
Применение моделей:
using(MobileContext db = new MobileContext())
{
db.Phones.Add(new Phone {Name = "Samsung Galaxy S5", Company = "Samsung", Price
= 14000 });
db.Phones.Add(new Phone {Name = "Nokia Lumia 630", Company = "Nokia", Price =
8000 });
Smartphone s1 = new Smartphone { Name = "iPhone 6", Company = "Apple", Price
= 32000, OS = "iOS" };
db.Smarts.Add(s1);
db.SaveChanges();
foreach (Phone p in db.Phones)
Console.WriteLine("{0} ({1}) - {2}", p.Name, p.Company, p.Price);
Console.WriteLine();
foreach (Smartphone p in db.Smarts)
Console.WriteLine("{0} ({1}, {2}) - {3}", p.Name, p.Company, p.Price, p.OS);
}
Несмотря на то, что объект Smartphone никак не связан с таблицей Phones, при извлечении
данных он также будет находится в наборе db.Phones, потому что наследование все равно
будет действовать.
Переходим к третьей части - Характеристическое сравнение :
Вот тут уже действительно интересно: В зависимости от ваших требований и требований
заказчика - здесь попросту нет 'лучшего' решения ... Но:
Движок Entity Framework хоть и поддерживает подход TPC , но для его адекватной работы
нам нужно метод OnModelCreating() переопределять , дабы фреймворк понял, что мы подвязываем
два класса между собою наследственными связями - чтобы можно было нормально работать
с результатом запросов используя этот подход в приложении .
Значит ли это то ,что в большинстве случаев нам будет придеться использовать 2-а
первых подхода - TPH и TPT ,чтобы не тратить время на изощрения с TPC и относительную
скорость работы приложения при получении запросов?
Давайте выясним!
Итак ,здесь мы использовать формат "критерий - условный победитель - почему" :
Скорость исполнения(работы) - TPH -> Таблица на одну иерархию классов в общем имеет
лучшую скорость хотя бы потому ,что на не надо делать запросы JOIN поскольку все данные
в одной таблице . Такое решение становиться даже более очевидным ,когда у нас наследственная
иерархия делается "шире" и "глубже".
Гибкость - TPT -> Таблица на тип более гибкая потому,что решает проблему редактирования
и обновлений колонок дочерней-таблицы при этом не изменяя родительскую таблицу.
Эстетичность - TPT -> это уже чисто субъективное мнение ,но как по-мне ,то TPT выглядит
для мене более объектно-ориентированным подходом.
Использование памяти - TPT -> если у вас иерархия наследования имеет очень много
различных типов , то использование TPT позволит использовать данные ,которые имеют
много незаполненных полей , тем более ,если структура баз данных решает проблему множества
пустых полей ,то эта проблема вряд ли скажется на производительности работы запросов.
Как мы с вами можем заметить ,если вы или ваш заказчик знает на какой критерий делать
упор - то вы можете сделать очевидный выбор исходя из информации выше
Единственный нюанс состоит в том ,что в 90% случаев ставка идет на Производительность,поэтому
в большинстве случаев использование TPH(Таблица на одну иерархию) для оптимизации работы
запросов - будет оптимальней
Надеюсь ,я этой статьей смог внести немного ясности в вопрос - если у вас есть пожелания
,то комментарии всегда открыты для вас - Спасибо за внимание,комрады!
Само собой ссылки на источники : практические примеры и использование данных подходов
в приложениях + отличительные характеристики каждого из подходов