具有TPH和枚举的实体框架中的多个CASE WHEN

本文关键字:CASE WHEN 实体 TPH 枚举 具有 框架 | 更新日期: 2023-09-27 18:24:05

在EF 6.1.3上使用TPH时,我有一个非常奇怪的行为。下面是一个基本的复制示例:

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

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作为X)的成本外,在我的项目中,查询很大(>50 KB),因为我有很多派生类,包含很多属性。正如您所料,我的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属性。这是可行的,但有两个属性用于相同的目的是非常令人困惑的。

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.这似乎还没有解决。

具有TPH和枚举的实体框架中的多个CASE WHEN

关于使用EF/大型查询

我已经对EF6和半大型层次结构做了一些工作。有几件事你应该考虑。首先,为什么您的DBA团队对此类查询不满意。当然,这些不是他们会写的查询,但假设管理层不希望你花时间从头开始写每一个查询,他们将不得不接受这样一个事实,即你使用ORM框架,而ORM框架可能会导致更大的查询。

现在,如果他们有特定的性能问题,你应该解决这些问题。

你能做什么

现在,您可以做些什么来清理查询。

1) 使所有可能是抽象抽象的类。

2) 将所有其他类密封。

3) 在linq中,查询尽可能转换为具体类型(使用OfType())。这甚至可能比.Select(x=>x作为SomethingHere)更有效。如果您有一个特别讨厌的查询,那么可能需要一些实验来优化linq中的查询。

解释我通过实验发现的东西

正如您在查询中注意到的那样,它正在检查鉴别器。如果您的查询变得有点复杂(我希望这5万个查询就是其中之一),您会看到它添加了用于字符串连接的代码,以检查每一个可能的组合。你可以在中看到这种情况

THEN '0X' WHEN ([Extent1].[Discriminator] = N'TypeA') THEN '0X0X' 

部分。我做了一些POC,试图弄清楚这种行为,似乎正在发生的是,实体框架正在将属性转换为"方面"(我的术语)。例如,如果转换的字符串包含"0X"或"0X0X",则类将具有"PropertyA"。PropertyB,它可能转换为"R2D2",PropertyC转换为"C3P0"。如果一个类名被翻译成"R2D2C3P0"。它知道它同时拥有PropertyB&财产C。它必须考虑一些隐藏的派生类型和所有超类型。现在,如果enity框架可以更确定您的类层次结构(通过使类密封),它可以简化这里的逻辑。根据我的经验,EF生成的字符串构建逻辑可能比您在这里展示的更复杂。这就是为什么使类抽象/密封EF可以更智能地处理这一问题并减少查询的原因。

另一个性能提示

现在还要确保在鉴别器列上有适当的索引。(您可以在实体框架内的DbMigration脚本中执行此操作)。

"esparate"性能度量

现在,如果所有其他操作都失败了,请将鉴别器设置为int。这会极大地损害数据库/查询的可读性,但有助于提高性能。(您甚至可以让所有类自动发出一个包含类名的属性,以便在数据库中保持类型的可读性)。

更新:

在RX_DID_RX的评论之后进行了更多的研究,结果发现,如果你不使用动态代理生成,你只能密封/制作poco的摘要。(延迟加载和更改跟踪)。在我的特定应用程序中,我们没有使用它,所以它对我们很有效,但我必须恢复我之前的建议。

有关EF6特定链接的更多详细信息http://www.entityframeworktutorial.net/Types-of-Entities.aspx

不过,在linq查询中添加索引和进行强制转换仍然会有所帮助。

来自巴达维亚的关于查询的回答:"现在,如果他们有特定的性能问题,你应该解决这些问题",不要在其他查询上浪费时间。此外,不要浪费时间去理解EF生成查询的原因(如果您使用Include跟踪LINQ查询,您会对生成的查询留下负面印象)
您需要处理的其他查询是与EF提供程序不兼容的查询(比如有时EF生成的带有CROSS JOIN的查询)。

关于SQL语句的性能(在DML中,您还可以在stackoverflow上找到其他几个问题):
-如果需要,可以使用存储过程
-EF中缺少一个特征。不能运行SQL查询并使用EF中定义的映射将其映射到类。您可以在这里找到SqlQuery的实体框架代码优先配置映射的实现(实际上,它可能需要一些修复才能使用TPH)。