EF Code First如何改善自引用和一对多关系的性能问题

5
我有一个自引用实体“AccountGroup”。叶子“AccountGroup”可以包含1个或多个“Accounts”。两个实体都有“Balance”属性。每个“AccountGroup”都有一个“Balance”,它要么是子组中“Balance”的总和,要么是所有帐户的“Balance”的总和(在叶子组的情况下)。
为了构建所有“AccountGroup”和“Account”的树形列表,我必须递归遍历此对象图,这会导致大量(我的意思是很多!)对数据库的调用...
有没有办法改进这种方式,以减少数据库调用次数?
谢谢
以下是精简的代码

帐户(仅属于1个帐户组)

public class Account
{
    public int Id { get; set; }
    public int GroupId { get; set; }
    public string Name { get; set; }
    public decimal Balance { get; set; }
    public string AccountType { get; set; }

    public virtual AccountGroup Group { get; set; }
}

账户组(如果是叶子节点,则有0个或多个账户组,有1个或多个账户)

public class AccountGroup
{
    public AccountGroup()
    {
        Accounts = new HashSet<Account>();
        Groups = new HashSet<AccountGroup>();
    }

    public int Id { get; set; }
    public bool IsRoot { get { return Parent == null; } }
    public bool IsLeaf { get { return !Groups.Any(); } }
    public decimal Balance { get { return IsLeaf ? Accounts.Sum(a => a.Balance) : Groups.Sum(g => g.Balance); } } // if leaf group, get sum of all account balances, otherwise get sum of all subgroups
    public int? ParentId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public virtual ISet<Account> Accounts { get; private set; }
    public virtual ISet<AccountGroup> Groups { get; private set; }
    public virtual AccountGroup Parent { get; set; }
}

调用代码

// start processing root groups (ones without parent)
foreach (var rootGroup in db.AccountGroups.Include(g=>g.Groups).Where(g => g.ParentId == null))
{
    TraverseAccountGroup(rootGroup, 0);
}

// recursive method
private static void TraverseAccountGroup(AccountGroup accountGroup, int level)
{
    //
    // process account group
    //
    Console.WriteLine("{0}{1} ({2})", String.Empty.PadRight(level * 2, '.'), accountGroup.Name, level);
    //
    // if subgroups exist, process recursivelly
    //
    if (accountGroup.Groups.Any())
    {
        foreach (var subGroup in accountGroup.Groups)
        {
            TraverseAccountGroup(subGroup, level + 1);
        }
    }
    //
    // otherwise, process accounts belonging to leaf subgroup
    //
    else
    {
        foreach (var account in accountGroup.Accounts)
        {
            Console.WriteLine("ACCOUNT [{0}]", account.Name);
        }
    }
}

我通常使用包含公共表达式(CTE)的数据库视图来进行分层查询。 - Ladislav Mrnka
@Ladislav Mrnka 我已经有了可以完成工作的CTE,但是EF Code First不支持它(至少在v5中还没有支持)...我可以“退步”到我的存储库并使用dbContext.Database.SqlQuery<>,但我希望有一种方法通过ORM来完成。 - zam6ak
是的,Code First 在某种程度上有所限制,但您仍然可以“欺骗”它,并以与表相同的方式映射视图(如果您不希望 EF 为您生成数据库-在这种情况下,它更加复杂,但仍然可以实现)。当涉及到性能问题时,通常是您必须放弃架构纯度,只是为了让事情正常工作,因此 SqlQuery 仍然是一个有效的选项。 - Ladislav Mrnka
@Ladislav Mrnka,我同意你的看法,也许没有其他选择...如果我创建一个视图,我是否需要创建一个帮助类(不是我的领域实体),并在其中某种方式附加/生成实体...对此任何帮助将不胜感激。 - zam6ak
1个回答

0

CTE方法

提高树形数据类型查询速度有两种方法。第一种(也可能是最简单的)是使用存储过程和EF的执行SQL功能来加载树形结构。SProc将缓存,结果集执行速度将增加。我建议在Sproc中使用递归CTE进行查询。

http://msdn.microsoft.com/en-us/library/ms186243(v=sql.105).aspx

with <CTEName> as
(
     SELECT
         <Root Query>
     FROM <TABLE>

     UNION ALL

     SELECT
         <Child Query>
     FROM <TABLE>
     INNER JOIN <CTEName>
         ON <CTEJoinCondition>
     WHERE 
          <TERMINATION CONDITION>

)

编辑

使用以下方式执行您的存储过程或内联CTE:

DbContext ctx = new SampleContext();
ctx.Database.SqlQuery<YourEntityType>(@"SQL OR SPROC COMMAND HERE", new[] { "Param1", "Param2", "Etc" });

扁平化您的树形结构

第二种方法是构建树的扁平表示。您可以将树扁平化为一个平面结构,以便快速查询,然后使用平面结构和实际树节点之间的链接来剪切自引用实体。您可以使用上述递归CTE查询构建平面结构。

这只是一种方法,但有许多关于此主题的论文:

http://www.governor.co.uk/news-plus-views/2010/5/17/depth-first-tree-flattening-with-the-yield-keyword-in-c-sharp/

编辑:添加额外的澄清

需要注意的是,递归CTE在迭代结构之前缓存查询的符号。这是解决您的问题的最快、最简单的方法。然而,这必须是一个SQL查询。您可以直接使用execute sql或者执行SProc。Sprocs在运行后缓存执行图表,因此它们比必须在运行之前构建执行计划的本机查询性能更好。这完全取决于您。

使用扁平化表示树的问题在于,您必须经常重建或不断维护扁平化结构。根据您的查询路径,确定应该使用哪种扁平化算法,但最终结果仍然相同。扁平化结构是在EF内“实现”您想要做的唯一方法,而无需通过DBConnection执行原始SQL来欺骗。


我更喜欢使用CTE,但是根据我对@Ladislav Mrnka的评论,在我使用CTE创建存储过程后,我该如何将其与其他域实体集成?我是在SQL级别上创建全新的域实体并进行管理,还是创建一个帮助类来映射1:1到CTE结果,然后在我的存储库级别上使用它来构建/附加域实体?是否有此方面的示例? - zam6ak
另外,在展开树的示例中,这是否也会生成多个查询?我同意这比我现有的递归方法更好,但是无论是否使用yield,我认为循环都会导致多个查询...不是吗? - zam6ak
将树展平应该能够在一次数据库往返中执行。您可以使用_context.Set<FlatTree>().Include(x => x.ActualTreeNode).Include(x => x.OtherObjectRefFromHere)加载其他视图。 - VulgarBinary
关于直接绑定到对象集合:DbContext ctx = new SampleContext(); ctx.Database.SqlQuery<YourEntityType>(@"SQL OR SPROC COMMAND HERE", new[] { "Param1", "Param2", "Etc" });然后,您可以使用.Include(...)来减少延迟加载的往返次数。该命令将加载类型,并将绑定连接到您设置的流畅配置。 - VulgarBinary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接