具有TPH和枚举的实体框架中的多个案例

Cyb*_*axs 14 .net c# entity-framework

在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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码完美无缺,但生成的查询非常奇怪而复杂,特别是有很多情况下的情况.

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]
Run Code Online (Sandbox Code Playgroud)

除了多个无用的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])
Run Code Online (Sandbox Code Playgroud)

更新2

一个常见的解决方案是创建一个单独的属性整数值并忽略枚举属性.这有效,但为同一目的而拥有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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

更新3

我在codeplex上发现了一个错误:https://entityframework.codeplex.com/workitem/2117 .它似乎没有解决.

Bat*_*via 2

关于使用 EF/Large 查询

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

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

你可以做什么

现在你可以做什么来清理你的查询。

1)将所有可以抽象的类抽象化。

2) 密封所有其他类。

3)在您的 linq 查询中,尽可能转换为具体类型(使用 OfType() )。这甚至可能比 .Select(x => x as SomethingHere) 更好。如果您有一个特定的令人讨厌的查询,可能需要进行一些实验才能最好地从 linq 调整您的查询。

解释我通过实验发现的内容

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

THEN '0X' WHEN ([Extent1].[Discriminator] = N'TypeA') THEN '0X0X' 
Run Code Online (Sandbox Code Playgroud)

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

另一个性能提示

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

“绝望”的绩效衡量标准

现在,如果一切都失败了,请让你的鉴别器成为一个 int 。这会严重损害数据库/查询的可读性,但它有助于提高性能。(您甚至可以让所有类自动发出一个包含类名的属性,以便您在数据库中保持类型的一些可读性)。

更新:

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

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

添加索引,以及在 linq 查询中进行转换仍然有帮助。