#c# #entity-framework #entity-framework-6
#c# #entity-framework #entity-framework-6
Вопрос:
Сначала у нас есть базовый объект с 10 дочерними объектами и кодом EF6.
Из этих 10 дочерних объектов 5 имеют только несколько (дополнительных) свойств, а 5 имеют несколько свойств (от 5 до 20). Мы реализовали это как таблицу для каждого типа, поэтому у нас есть одна таблица для базы и 1 для дочернего объекта (всего 10).
Это, однако, создает ОГРОМНЫЕ запросы select с select case
и unions
повсюду, что также требует 6 секунд EF для генерации (в первый раз).
Я читал об этой проблеме и о том, что та же проблема сохраняется в сценарии типа «таблица для конкретного типа».
Итак, у нас остается таблица для иерархии, но это создает таблицу с большим количеством свойств, что тоже звучит не очень.
Есть ли другое решение для этого?
Я подумал о том, чтобы пропустить наследование и создать представление объединения, когда я хочу получить все элементы из всех дочерних объектов / записей.
Есть еще мысли?
Комментарии:
1. являются ли дочерние объекты коллекциями?
2. Нет, это структура наследования c #. У нас есть базовый класс
a
и классb
,c
,d
,e
,f
,g
все наследуют отa
(и только отa
)3. Моим выводом к той же проблеме была одна большая база данных, что, я думаю, является сильным признаком того, что мне, возможно, лучше отказаться от EF и перейти на решение без sql. У меня также есть много связей между дочерними типами, поэтому я рассматриваю graph-db (возможно, Neo4j).
Ответ №1:
Другим решением было бы реализовать какой-то шаблон CQRS, в котором у вас есть отдельные базы данных для записи (команда) и чтения (запрос). Вы даже можете де-нормализовать данные в базе данных для чтения, чтобы это было очень быстро.
Предполагая, что вам нужна хотя бы одна нормализованная модель со ссылочной целостностью, я думаю, что ваше решение действительно сводится к таблице для каждой иерархии и таблице для каждого типа. Алекс Джеймс из команды EF и совсем недавно на сайте Microsoft Data Development сообщает, что TPH обеспечивает лучшую производительность.
Преимущества TPT и почему они не так важны, как производительность:
Большая гибкость, что означает возможность добавлять типы, не затрагивая ни одну существующую таблицу. Не слишком большая проблема, потому что миграции EF делают тривиальной генерацию требуемого SQL для обновления существующих баз данных без ущерба для данных.
Проверка базы данных из-за наличия меньшего количества полей с нулевым значением. Не является серьезной проблемой, поскольку EF проверяет данные в соответствии с моделью приложения. Если данные добавляются другими способами, не так уж сложно запустить фоновый скрипт для проверки данных. Кроме того, TPT и TPC на самом деле хуже для проверки, когда дело доходит до первичных ключей, потому что две таблицы подклассов потенциально могут содержать один и тот же первичный ключ. Вы остаетесь с проблемой проверки другими средствами.
Пространство для хранения сокращается из-за отсутствия необходимости хранить все нулевые поля. Это всего лишь очень тривиальная проблема, особенно если СУБД имеет хорошую стратегию для обработки «разреженных» столбцов.
Дизайн и интуиция. Наличие одной очень большой таблицы кажется немного неправильным, но это, вероятно, потому, что большинство разработчиков БД потратили много часов на нормализацию данных и создание ERD. Наличие одной большой таблицы, похоже, противоречит основным принципам проектирования базы данных. Это, вероятно, самый большой барьер для TPH. Смотрите Эту статью для особенно страстного аргумента.
В этой статье обобщен основной аргумент против TPH как:
Он не нормализован даже в тривиальном смысле, это делает невозможным обеспечение целостности данных и, что самое «удивительное»: он практически гарантированно плохо работает в больших масштабах для любого нетривиального набора данных.
В основном это неверно. Производительность и целостность упомянуты выше, и TPH не обязательно означает денормализованный. Существует только много (обнуляемых) столбцов внешнего ключа, которые являются самоссылочными. Таким образом, мы можем продолжить проектирование и нормализацию данных точно так же, как и в случае TPH. В текущей базе данных у меня много связей между подтипами, и я создал ERD, как если бы это была структура наследования TPT. Это фактически отражает реализацию в code-first Entity Framework. Например, вот мой Expenditure
класс, который наследует от Relationship
которого наследует от Content
:
public class Expenditure : Relationship
{
/// <summary>
/// Inherits from Content: Id, Handle, Description, Parent (is context of expenditure and usually
/// a Project)
/// Inherits from Relationship: Source (the Principal), SourceId, Target (the Supplier), TargetId,
///
/// </summary>
[Required, InverseProperty("Expenditures"), ForeignKey("ProductId")]
public Product Product { get; set; }
public Guid ProductId { get; set; }
public string Unit { get; set; }
public double Qty { get; set; }
public string Currency { get; set; }
public double TotalCost { get; set; }
}
InversePropertyAttribute
И ForeignKeyAttribute
предоставляют EF информацию, необходимую для создания требуемых самосоединений в единой базе данных.
Тип продукта также сопоставляется с той же таблицей (также наследуется от содержимого). Каждый продукт имеет свою собственную строку в таблице, а строки, содержащие расходы, будут включать данные в ProductId
столбец, который равен null для строк, содержащих все другие типы. Таким образом, данные нормализуются, просто помещаются в одну таблицу.
Прелесть использования EF code first заключается в том, что мы проектируем базу данных точно таким же образом и реализуем ее (почти) точно так же, независимо от использования TPH или TPT. Чтобы изменить реализацию с TPH на TPT, нам просто нужно добавить аннотацию к каждому подклассу, сопоставив их с новыми таблицами. Итак, хорошая новость для вас в том, что на самом деле не имеет значения, какой из них вы выберете. Просто создайте его, сгенерируйте стек тестовых данных, протестируйте его, измените стратегию, протестируйте еще раз. Я думаю, вы найдете TPH победителем.
Комментарии:
1. Я немного опоздал на вечеринку, но это все еще обсуждается. Вы не упомянули индексирование и использование этого в качестве профессионала на стороне TPT. Есть ли способы обойти массивную таблицу с плохой индексацией в TPH? Что-то еще, что я упускаю из виду, поскольку я не являюсь администратором базы данных по профессии?
Ответ №2:
Сам столкнувшись с подобными проблемами, у меня есть несколько предложений. Я также открыт для улучшения этих предложений, поскольку это сложная тема, и у меня еще не все разработано.
Entity framework может работать очень медленно при работе с нетривиальными запросами к сложным объектам, то есть с несколькими уровнями дочерних коллекций. В некоторых тестах производительности, которые я пробовал, он ужасно долго компилирует запрос. Теоретически EF 5 и последующие версии должны кэшировать скомпилированные запросы (даже если контекст удаляется и создается заново) без необходимости что-либо делать, но я не уверен, что это всегда так.
Я прочитал несколько предложений о том, что вам следует создать несколько DataContexts только с меньшими подмножествами объектов вашей базы данных для сложной базы данных. Если это практично для вас, попробуйте! Но я полагаю, что при таком подходе могут возникнуть проблемы с обслуживанием.
1) Я знаю, что это очевидно, но в любом случае стоит сказать — убедитесь, что в вашей базе данных установлены правильные внешние ключи для связанных объектов, так как тогда entity framework будет отслеживать эти отношения и будет намного быстрее генерировать запросы, в которых вам нужно присоединиться, используя внешний ключ.
2) Не извлекайте больше, чем вам нужно. Универсальные методы для получения сложного объекта редко бывают оптимальными. Допустим, вы получаете список базовых объектов (для добавления в список), и вам нужно только отобразить имя и идентификатор этих объектов в списке базового объекта. Просто извлеките только базовый объект — любые свойства навигации, которые специально не нужны, не должны извлекаться.
3) Если дочерние объекты не являются коллекциями, или они являются коллекциями, но вам нужен только 1 элемент (или совокупное значение, такое как количество) из них, я бы обязательно реализовал представление в базе данных и запросил его вместо этого. Это НАМНОГО быстрее. EF не должен выполнять никакой работы — все это делается в базе данных, которая лучше оборудована для такого типа операций.
4) Будьте осторожны с .Включить () и это восходит к пункту № 2 выше. Если вы получаете один объект свойство дочерней коллекции, вам лучше не использовать.Включить (), так как тогда, когда дочерняя коллекция будет извлечена, это будет сделано как отдельный запрос. (таким образом, не получить все столбцы базового объекта для каждой строки в дочерней коллекции)
Редактировать
После комментариев вот некоторые дополнительные мысли.
Поскольку мы имеем дело с иерархией наследования, логично хранить отдельные таблицы для дополнительных свойств наследующих классов таблица для базового класса. Вопрос о том, как заставить Entity Framework работать хорошо, все еще обсуждается.
Я использовал EF для аналогичного сценария (но с меньшим количеством дочерних объектов) (сначала база данных), но в этом случае я не использовал фактические классы, созданные Entity Framework, в качестве бизнес-объектов. Объекты EF напрямую связаны с таблицами БД.
Я создал отдельные бизнес-классы для базового и наследующего классов, а также набор картографов, которые будут преобразовываться в них. Запрос будет выглядеть примерно так
public static List<BaseClass> GetAllItems()
{
using (var db = new MyDbEntities())
{
var q1 = db.InheritedClass1.Include("BaseClass").ToList()
.ConvertAll(x => (BaseClass)InheritedClass1Mapper.MapFromContext(x));
var q2 = db.InheritedClass2.Include("BaseClass").ToList()
.ConvertAll(x => (BaseClass)InheritedClass2Mapper.MapFromContext(x));
return q1.Union(q2).ToList();
}
}
Не говорю, что это лучший подход, но это может быть отправной точкой?
В этом случае запросы, безусловно, быстро компилируются!
Комментарии приветствуются!
Комментарии:
1. О том, что «Теоретически EF 5 и более поздние версии должны кэшировать скомпилированные запросы», я могу сказать, что в нашем случае это так, потому что мы можем легко заметить это … потому что в первый раз это занимает минуты, а в следующий раз выполняется немедленно
2. Я думаю, это то, на что стоит обратить внимание в нашей ситуации: «Не извлекайте больше, чем вам нужно»
3. Я не уверен, что этот ответ — это то, что ищет OP. У него большая иерархия наследования, и ему интересно, какой наилучший подход: таблица для каждой иерархии, таблица для каждого типа или таблица для каждого конкретного типа.
4. @Dismissile Вы правы, как бы я ни был доволен своими мыслями, это был не совсем ответ 😉
5. спасибо, на самом деле это был не ответ — скорее набор мыслей 🙂 Теперь я добавил немного больше, что более актуально для вашей ситуации.
Ответ №3:
С таблицей на иерархию вы получаете только одну таблицу, поэтому, очевидно, ваши операции CRUD будут быстрее, и эта таблица в любом случае абстрагируется вашим доменным уровнем. Недостатком является то, что вы теряете возможность для ограничений NOT NULL, поэтому ваш бизнес-уровень должен правильно обрабатываться, чтобы избежать потенциальной целостности данных. Кроме того, добавление или удаление объектов означает, что таблица изменяется; но это также то, что поддается управлению.
С таблицей для каждого типа у вас есть проблема, заключающаяся в том, что чем больше классов у вас в иерархии, тем медленнее будут ваши операции CRUD.
В целом, поскольку производительность, вероятно, является наиболее важным фактором здесь, и у вас много классов, я думаю, что таблица на иерархию является победителем как с точки зрения производительности, так и простоты, а также с учетом вашего количества классов.
Также посмотрите эту статью, более конкретно в главе 7.1.1 (Избегание TPT в приложениях Model First или Code First), где говорится: «при создании приложения с использованием Model First или Code First вам следует избегать наследования TPT из соображений производительности».
Ответ №4:
Модель EF6 CodeFirst, над которой я работаю, использует обобщенные и абстрактные базовые классы под названием «BaseEntity». Я также использую дженерики и базовый класс для класса EntityTypeConfiguration.
В случае, если мне нужно повторно использовать пару свойств «столбцы» в некоторых таблицах, и для них не имеет смысла использовать BaseEntity или BaseEntityWithMetaData, я создаю для них интерфейс.
Например, у меня есть один для адресов, которые я еще не закончил. Итак, если у объекта есть адресная информация, он будет реализовывать IAddressInfo. Приведение объекта к IAddressInfo даст мне объект только с AddressInfo на нем.
Первоначально у меня были столбцы метаданных в виде собственной таблицы. Но, как упоминали другие, запросы были ужасающими, и это было медленнее, чем медленно. Итак, я подумал, почему бы мне просто не использовать несколько путей наследования для поддержки того, что я хочу сделать, чтобы столбцы были в каждой таблице, которая в них нуждается, а не в тех, которые этого не делают. Также я использую mysql, у которого ограничение по столбцу 4096. Sql Server 2008 имеет 1024. Даже при 1024 я не вижу реалистичных сценариев для перехода к этому в одной таблице.
И ни один из моих objjets не наследуется таким образом, что у них есть столбцы, которые им не нужны. Когда возникает такая необходимость, я создаю новый базовый класс на уровне, чтобы предотвратить появление дополнительных столбцов.
Вот достаточно фрагментов из моего кода, чтобы понять, как у меня настроено наследование. Пока это работает очень хорошо для меня. На самом деле я не создал сценарий, который я не мог бы смоделировать с помощью этой настройки.
public BaseEntityConfig<T> : EntityTypeConfiguration<T> where T : BaseEntity<T>, new()
{
}
public BaseEntity<T> where T : BaseEntity<T>, new()
{
//shared properties here
}
public BaseEntityMetaDataConfig : BaseEntityConfig<T> where T: BaseEntityWithMetaData<T>, new()
{
public BaseEntityWithMetaDataConfig()
{
this.HasOptional(e => e.RecCreatedBy).WithMany().HasForeignKey(p => p.RecCreatedByUserId);
this.HasOptional(e => e.RecLastModifiedBy).WithMany().HasForeignKey(p => p.RecLastModifiedByUserId);
}
}
public BaseEntityMetaData<T> : BaseEntity<T> where T: BaseEntityWithMetaData<T>, new()
{
#region Entity Properties
public DateTime? DateRecCreated { get; set; }
public DateTime? DateRecModified { get; set; }
public long? RecCreatedByUserId { get; set; }
public virtual User RecCreatedBy { get; set; }
public virtual User RecLastModifiedBy { get; set; }
public long? RecLastModifiedByUserId { get; set; }
public DateTime? RecDateDeleted { get; set; }
#endregion
}
public PersonConfig()
{
this.ToTable("people");
this.HasKey(e => e.PersonId);
this.HasOptional(e => e.User).WithRequired(p => p.Person).WillCascadeOnDelete(true);
this.HasOptional(p => p.Employee).WithRequired(p => p.Person).WillCascadeOnDelete(true);
this.HasMany(e => e.EmailAddresses).WithRequired(p => p.Person).WillCascadeOnDelete(true);
this.Property(e => e.FirstName).IsRequired().HasMaxLength(128);
this.Property(e => e.MiddleName).IsOptional().HasMaxLength(128);
this.Property(e => e.LastName).IsRequired().HasMaxLength(128);
}
}
//I Have to use this pattern to allow other classes to inherit from person, they have to inherit from BasePeron<T>
public class Person : BasePerson<Person>
{
//Just a dummy class to expose BasePerson as it is.
}
public class BasePerson<T> : BaseEntityWithMetaData<T> where T: BasePerson<T>, new()
{
#region Entity Properties
public long PersonId { get; set; }
public virtual User User { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public virtual Employee Employee { get; set; }
public virtual ICollection<PersonEmail> EmailAddresses { get; set; }
#endregion
#region Entity Helper Properties
[NotMapped]
public PersonEmail PrimaryPersonalEmail
{
get
{
PersonEmail ret = null;
if (this.EmailAddresses != null)
ret = (from e in this.EmailAddresses where e.EmailAddressType == EmailAddressType.Personal_Primary select e).FirstOrDefault();
return ret;
}
}
[NotMapped]
public PersonEmail PrimaryWorkEmail
{
get
{
PersonEmail ret = null;
if (this.EmailAddresses != null)
ret = (from e in this.EmailAddresses where e.EmailAddressType == EmailAddressType.Work_Primary select e).FirstOrDefault();
return ret;
}
}
private string _DefaultEmailAddress = null;
[NotMapped]
public string DefaultEmailAddress
{
get
{
if (string.IsNullOrEmpty(_DefaultEmailAddress))
{
PersonEmail personalEmail = this.PrimaryPersonalEmail;
if (personalEmail != null amp;amp; !string.IsNullOrEmpty(personalEmail.EmailAddress))
_DefaultEmailAddress = personalEmail.EmailAddress;
else
{
PersonEmail workEmail = this.PrimaryWorkEmail;
if (workEmail != null amp;amp; !string.IsNullOrEmpty(workEmail.EmailAddress))
_DefaultEmailAddress = workEmail.EmailAddress;
}
}
return _DefaultEmailAddress;
}
}
#endregion
#region Constructor
static BasePerson()
{
}
public BasePerson()
{
this.User = null;
this.EmailAddresses = new HashSet<PersonEmail>();
}
public BasePerson(string firstName, string lastName)
{
this.FirstName = firstName;
this.LastName = lastName;
}
#endregion
}
Теперь код в контексте создания модели выглядит следующим образом,
//Config
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
//initialize configuration, each line is responsible for telling entity framework how to create relation ships between the different tables in the database.
//Such as Table Names, Foreign Key Contraints, Unique Contraints, all relations etc.
modelBuilder.Configurations.Add(new PersonConfig());
modelBuilder.Configurations.Add(new PersonEmailConfig());
modelBuilder.Configurations.Add(new UserConfig());
modelBuilder.Configurations.Add(new LoginSessionConfig());
modelBuilder.Configurations.Add(new AccountConfig());
modelBuilder.Configurations.Add(new EmployeeConfig());
modelBuilder.Configurations.Add(new ContactConfig());
modelBuilder.Configurations.Add(new ConfigEntryCategoryConfig());
modelBuilder.Configurations.Add(new ConfigEntryConfig());
modelBuilder.Configurations.Add(new SecurityQuestionConfig());
modelBuilder.Configurations.Add(new SecurityQuestionAnswerConfig());
Причина, по которой я создал базовые классы для конфигурации моих объектов, заключалась в том, что, когда я начал этот путь, я столкнулся с досадной проблемой. Мне приходилось настраивать общие свойства для каждого производного класса снова и снова. И если я обновил одно из отображений fluent API, мне пришлось обновлять код в каждом производном классе.
Но при использовании этого метода наследования в классах конфигурации два свойства настраиваются в одном месте и наследуются классом конфигурации для производных объектов.
Итак, когда PeopleConfig настроен, он запускает логику в классе BaseEntityWithMetaData для настройки двух свойств, и снова при запуске UserConfig и т. Д. И т. Д. И т. Д.
Комментарии:
1. Квинтэссенция вашего подхода заключается в том, что EF не знает о базовом классе. Может быть, вы можете сделать на этом больший акцент, потому что я думаю, что в большинстве случаев, когда задействован базовый класс, это лучшее, что можно сделать. Если только речь не идет о реальном наследовании (ассоциация «есть»), но очень часто наследованием злоупотребляют для реализации сквозных проблем.
Ответ №5:
Три разных подхода имеют разные названия на языке М. Фаулера:
Single Table inheritance
— вся иерархия наследования хранится в одной таблице. Нет объединений, необязательные столбцы для дочерних типов. Вам нужно различать, какой это дочерний тип.Concrete Table inheritance
— у вас есть одна таблица для каждого конкретного типа. Объединения, никаких необязательных столбцов. В этом случае таблица базового типа необходима только в том случае, если для базового типа требуется собственное отображение (экземпляр может быть создан).Class Table inheritance
— у вас есть таблица базового типа и дочерние таблицы, каждая из которых добавляет только дополнительные столбцы к столбцам базы. Объединения, никаких необязательных столбцов. В этом случае таблица базового типа всегда содержит строку для каждого дочернего объекта; однако вы можете извлекать общие столбцы, только если не требуются столбцы, относящиеся к конкретному дочернему объекту (возможно, остальное поставляется с отложенной загрузкой?).
Все подходы работоспособны — это зависит только от объема и структуры имеющихся у вас данных, поэтому вы можете сначала измерить различия в производительности.
Выбор будет зависеть от количества объединений, распределения данных и необязательных столбцов.
- Если у вас нет (и не будет) много дочерних типов, я бы выбрал наследование таблицы классов, поскольку оно близко к домену и будет легко переводиться / сопоставляться.
- Если у вас есть много дочерних таблиц для одновременной работы и вы ожидаете узких мест в объединениях — используйте наследование одной таблицы.
- Если объединения вообще не нужны, и вы собираетесь работать с одним конкретным типом за раз — используйте наследование конкретной таблицы.
Ответ №6:
Хотя таблица на иерархию (TPH) является лучшим подходом для быстрых операций CRUD, в этом случае невозможно избежать одной таблицы с таким количеством свойств для созданной базы данных. Упомянутые вами предложения case и union создаются потому, что результирующий запрос фактически запрашивает полиморфный результирующий набор, включающий несколько типов.
Однако, когда EF возвращает сплющенную таблицу, содержащую данные для всех типов, она выполняет дополнительную работу, чтобы гарантировать, что для столбцов, которые могут быть неактуальны для определенного типа, возвращаются значения null. Технически, эта дополнительная проверка с использованием регистра и объединения не требуется. Приведенная ниже проблема связана с сбоем производительности в Microsoft EF6, и они намерены внести это исправление в будущую версию.
Приведенный ниже запрос:
SELECT
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[Name] AS [Name],
[Extent1].[Address] AS [Address],
[Extent1].[City] AS [City],
CASE WHEN (( NOT (([UnionAll1].[C3] = 1) AND ([UnionAll1].[C3] IS NOT NULL))) AND ( NOT(([UnionAll1].[C4] = 1) AND ([UnionAll1].[C4] IS NOT NULL)))) THEN CAST(NULL ASvarchar(1)) WHEN (([UnionAll1].[C3] = 1) AND ([UnionAll1].[C3] IS NOT NULL)) THEN[UnionAll1].[State] END AS [C2],
CASE WHEN (( NOT (([UnionAll1].[C3] = 1) AND ([UnionAll1].[C3] IS NOT NULL))) AND ( NOT(([UnionAll1].[C4] = 1) AND ([UnionAll1].[C4] IS NOT NULL)))) THEN CAST(NULL ASvarchar(1)) WHEN (([UnionAll1].[C3] = 1) AND ([UnionAll1].[C3] IS NOT NULL))THEN[UnionAll1].[Zip] END AS [C3],
FROM [dbo].[Customers] AS [Extent1]
может быть безопасно заменен на:
SELECT
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[Name] AS [Name],
[Extent1].[Address] AS [Address],
[Extent1].[City] AS [City],
[UnionAll1].[State] AS [C2],
[UnionAll1].[Zip] AS [C3],
FROM [dbo].[Customers] AS [Extent1]
Итак, вы только что увидели проблему и недостаток текущей версии Entity Framework 6, у вас есть возможность либо использовать подход, основанный на модели, либо использовать подход TPH.
Комментарии:
1. Знаете ли вы какой-либо способ настройки / исправления этой проблемы? Возможно, вручную отредактировав исходный код.