Несколько CASE КОГДА в Entity Framework с TPH и перечислением
У меня очень странное поведение при использовании TPH на EF 6.1.3.
Вот основной пример для воспроизведения:
public class BaseType
{
public int Id { get; set; }
}
public class TypeA : BaseType
{
public string PropA { get; set; }
}
public class TypeB : BaseType
{
public decimal PropB { get; set; }
public OneEnum PropEnum { get; set; }
}
public class TypeC : TypeB
{
public int PropC { get; set; }
}
public enum OneEnum
{
Foo,
Bar
}
public partial class EnumTestContext : DbContext
{
public EnumTestContext()
{
this.Database.Log = s => { Debug.WriteLine(s); };
}
public DbSet<BaseType> BaseTypes { get; set; }
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseAlways<EnumTestContext>());
using (var context = new EnumTestContext())
{
context.BaseTypes.Add(new TypeA() { Id = 1, PropA = "propA" });
context.BaseTypes.Add(new TypeB() { Id = 2, PropB = 4.5M, /*PropEnum = OneEnum.Bar*/ });
context.BaseTypes.Add(new TypeC() { Id = 3, PropB = 4.5M, /*PropEnum = OneEnum.Foo,*/ PropC = 123 });
context.SaveChanges();
var onetype = context.BaseTypes.Where(b => b.Id == 1).FirstOrDefault();
Console.WriteLine("typeof {0} with {1}", onetype.GetType().Name, onetype.Id);
}
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
Этот код работает отлично, но сгенерированный запрос экстремально странный и сложный, особенно много CASE WHEN
SELECT
[Limit1].[C1] AS [C1],
[Limit1].[Id] AS [Id],
[Limit1].[C2] AS [C2],
[Limit1].[C3] AS [C3],
[Limit1].[C4] AS [C4],
[Limit1].[C5] AS [C5]
FROM ( SELECT TOP (1)
[Extent1].[Id] AS [Id],
CASE WHEN ([Extent1].[Discriminator] = N'BaseType') THEN '0X' WHEN ([Extent1].[Discriminator] = N'TypeA') THEN '0X0X' WHEN ([Extent1].[Discriminator] = N'TypeB') THEN '0X1X' ELSE '0X1X0X' END AS [C1],
CASE WHEN ([Extent1].[Discriminator] = N'BaseType') THEN CAST(NULL AS varchar(1)) WHEN ([Extent1].[Discriminator] = N'TypeA') THEN [Extent1].[PropA] WHEN ([Extent1].[Discriminator] = N'TypeB') THEN CAST(NULL AS varchar(1)) END AS [C2],
CASE WHEN ([Extent1].[Discriminator] = N'BaseType') THEN CAST(NULL AS decimal(18,2)) WHEN ([Extent1].[Discriminator] = N'TypeA') THEN CAST(NULL AS decimal(18,2)) WHEN ([Extent1].[Discriminator] = N'TypeB') THEN [Extent1].[PropB] ELSE [Extent1].[PropB] END AS [C3],
CASE WHEN ([Extent1].[Discriminator] = N'BaseType') THEN CAST(NULL AS int) WHEN ([Extent1].[Discriminator] = N'TypeA') THEN CAST(NULL AS int) WHEN ([Extent1].[Discriminator] = N'TypeB') THEN [Extent1].[PropEnum] ELSE [Extent1].[PropEnum] END AS [C4],
CASE WHEN ([Extent1].[Discriminator] = N'BaseType') THEN CAST(NULL AS int) WHEN ([Extent1].[Discriminator] = N'TypeA') THEN CAST(NULL AS int) WHEN ([Extent1].[Discriminator] = N'TypeB') THEN CAST(NULL AS int) ELSE [Extent1].[PropC] END AS [C5]
FROM [dbo].[BaseTypes] AS [Extent1]
WHERE ([Extent1].[Discriminator] IN (N'TypeA',N'TypeB',N'TypeC',N'BaseType')) AND (1 = [Extent1].[Id])
) AS [Limit1]
За исключением нескольких и бесполезных THEN CAST (NULL as X), запрос большой ( > 50 КБ) в моем проекте, потому что у меня много производных классов, содержащих много свойств. Как и следовало ожидать, моя команда DBA не рада видеть подобные запросы в наших базах данных.
Если я удалю свойство перечисления в TypeB, запрос будет намного более чистым. То же самое, если у меня есть только два уровня иерархии, ака class TypeC : BaseType
(по сравнению с 3 в примере, потому что class TypeC : TypeB
).
Есть ли какие-либо настройки или конфигурации модели или обходные пути, чтобы избежать этого странного поведения?
Обновление
Вот сгенерированный запрос, если я удалю TypeB.PropEnum
SELECT TOP (1)
[Extent1].[Discriminator] AS [Discriminator],
[Extent1].[Id] AS [Id],
[Extent1].[PropA] AS [PropA],
[Extent1].[PropB] AS [PropB],
[Extent1].[PropC] AS [PropC]
FROM [dbo].[BaseTypes] AS [Extent1]
WHERE ([Extent1].[Discriminator] IN (N'TypeA',N'TypeB',N'TypeC',N'BaseType')) AND (1 = [Extent1].[Id])
Обновление 2
Общим решением является создание отдельного свойства целочисленное значение и игнорирование свойства enum. Это работает, но довольно запутанно иметь 2 свойства для этой же цели.
public class TypeB : BaseType
{
public decimal PropB { get; set; }
public int PropEnumValue { get; set; }
[NotMapped]
public OneEnum PropEnum
{
get { return (OneEnum)PropEnumValue; }
set { PropEnumValue = (int)value; }
}
}
Обновление 3
Я нашел ошибку на codeplex: https://entityframework.codeplex.com/workitem/2117. Это, похоже, не решается.
Ответы
Ответ 1
Об использовании EF/Large запросов
Я проделал некоторую работу с EF6 и полу-большими иерархиями. Есть несколько вещей, которые вы должны учитывать. Прежде всего, почему ваша команда DBA не довольна этими запросами. Конечно, это не те запросы, которые они пишут, но при условии, что управление не хочет, чтобы вы тратили время на то, чтобы писать каждый отдельный запрос с нуля, им придется жить с тем фактом, что вы используете структуру ORM и что структура ORM может вызывает запросы, которые немного больше.
Теперь, если у них есть определенные проблемы с производительностью, вы ДОЛЖНЫ адресовать их.
Что вы можете сделать
теперь что вы можете сделать, чтобы очистить ваши запросы.
1) Сделайте все классы абстрактными абстрактными.
2) Запечатайте все остальные классы.
3) В ваших запросах linq, где это возможно, используются конкретные типы (с использованием OfType()). Это может даже работать лучше, чем. Выбрать (x = > x как SomethingHere). Если у вас есть конкретный неприятный запрос, может потребоваться некоторое экспериментирование, что лучше всего настраивает ваш запрос из linq.
объяснение, что я нашел в ходе экспериментов
Как вы заметили, ваши запросы проверяют дискриминатор. Если ваши запросы немного усложняются (и я ожидаю, что эти 50k-запросы станут одним из них), вы увидите, что он добавляет код для конкатенации строк, чтобы проверять все возможные комбинации. вы видите, что это происходит немного в
THEN '0X' WHEN ([Extent1].[Discriminator] = N'TypeA') THEN '0X0X'
часть.
Я сделал несколько POC, пытающихся выяснить это поведение, и что, кажется, происходит, заключается в том, что инфраструктура entity преобразует свойства в "аспекты" (мой термин). Например, класс будет иметь свойство "PropertyA", если переведенная строка содержит либо "0X", либо "0X0X". СвойствоB может перевести на "R2D2" и PropertyC на "C3P0". таким образом, если имя класса переводится в "R2D2C3P0". он знает, что имеет как PropertyB, так и PropertyC. Он должен учитывать некоторые скрытые производные типы и все супертипы. Теперь, если инфраструктура enity может быть более уверенной в вашей иерархии классов (делая классы закрытыми), это может упростить логику здесь. И по моему опыту генерация строковой логики EF может быть еще сложнее, чем те, которые вы показываете здесь. Вот почему создание абстрактных/закрытых классов EF может быть более разумным и сократить ваши запросы.
Другие рекомендации по производительности
Теперь также убедитесь, что у вас есть соответствующие индексы в столбце дискриминатора. (Вы можете сделать это из своей внутренней инфраструктуры DbMigration script).
Показатель производительности "Desparate"
Теперь, если все остальное не удается сделать ваш дискриминатор int. Это повредит читабельности вашей базы данных/запросов LOT, но это поможет повысить производительность. (и вы даже можете, чтобы все ваши классы автоматически выдавали свойство, содержащее имя класса, чтобы вы сохраняли некоторую читаемость типов внутри вашей базы данных).
UPDATE:
после еще нескольких исследований после комментария от RX_DID_RX, вы можете только скрыть/сделать poco abstract, если вы не используете динамическое создание прокси. (ленивая загрузка и отслеживание изменений). В моем конкретном приложении мы не использовали это, так что это сработало для нас, но мне нужно вернуть прежнюю рекомендацию.
Для более подробной информации ссылка на EF6 http://www.entityframeworktutorial.net/Types-of-Entities.aspx
добавление индексов, а игра с кастингом в запросах linq все равно может помочь.
Ответ 2
Из Batavia ответьте на запросы: "Теперь, если у них есть определенные проблемы с производительностью, вы ДОЛЖНЫ обращаться к этим" и не тратьте время на другие запросы. Кроме того, не теряйте время, чтобы понять, почему EF генерирует запрос (если вы отслеживаете запросы LINQ с Include, вы будете отрицательно впечатлены сгенерированными запросами).
Другие запросы, которые необходимо задать, - это запрос, который несовместим с вашим провайдером EF (например, с запросами CROSS JOIN, которые иногда генерируются EF).
О производительности в операторах SQL (в DML вы можете найти несколько других вопросов также в stackoverflow):
- если вы хотите, вы можете использовать хранимые процедуры;
- в EF отсутствует функция. Вы не можете запустить SQL-запрос и сопоставить его с классом с помощью отображения, определенного в EF. Вы можете найти реализацию здесь Entity framework Code First - настроить сопоставление для SqlQuery (на самом деле это может потребоваться некоторое исправление для работы с TPH).