DB Intercept导致SQL嵌套太深

时间:2016-07-09 21:36:53

标签: sql-server entity-framework ef-code-first soft-delete

我在实体框架6(代码优先模型)中使用数据库拦截以实现软删除功能,但是间歇性地我从SQL Server抛出异常,说明“SQL语句的某些部分是嵌套的太深了。重写查询或将其分解为较小的查询。“

这是生成sql的一个例子,你可以看到生成的SQL非常离谱:

https://gist.github.com/junderhill/87caceac728809a8ca837b9d8b5189f3

我的EF拦截代码如下:

 public class SoftDeleteInterceptor : IDbCommandTreeInterceptor
{
    public const string IsDeletedColumnName = "IsDeleted";
    public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
    {
        if (interceptionContext.OriginalResult.DataSpace != DataSpace.SSpace)
        {
            return;
        }

        var queryCommand = interceptionContext.Result as DbQueryCommandTree;
        if (queryCommand != null)
        {
            interceptionContext.Result = HandleQueryCommand(queryCommand);
        }
    }

    private static DbCommandTree HandleQueryCommand(DbQueryCommandTree queryCommand)
    {
        var newQuery = queryCommand.Query.Accept(new SoftDeleteQueryVisitor());
        return new DbQueryCommandTree(
            queryCommand.MetadataWorkspace,
            queryCommand.DataSpace,
            newQuery);
    }

    public class SoftDeleteQueryVisitor : DefaultExpressionVisitor
    {
        public override DbExpression Visit(DbScanExpression expression)
        {
            var table = (EntityType)expression.Target.ElementType;
            if (table.Properties.All(p => p.Name != IsDeletedColumnName))
            {
                return base.Visit(expression);
            }

            var binding = expression.Bind();
            return binding.Filter(
                binding.VariableType
                    .Variable(binding.VariableName)
                    .Property(IsDeletedColumnName)
                    .NotEqual(DbExpression.FromBoolean(true)));
        }
    }

1 个答案:

答案 0 :(得分:4)

<强>更新

这确实有效。然而,随着时间的推移,我们的服务器出现了严重的减速。每次我们重置IIS应用程序池时,它都会很快,但看起来EF会以某种方式缓存或泄漏内存并减慢它的速度。我们暂时把拦截器拉了出来。

我们在工作中遇到了这个问题,经过一段时间后,这里有一个解决方案(尚未经过广泛测试;如果需要,我会更新此更新):

要从根{1}开始,如果您需要添加过滤器,则会返回SoftDeleteQueryVisitor,从而修改查询。这又会触发return binding.Filter(...)。这会遍历树,再次找到TreeCreated,然后重新添加过滤器。显而易见的解决方案是,如果已经有过滤器,则不添加过滤器。

查看树的结构,我们发现过滤器是扫描的父节点:

DbScanExpression

然而,一旦我们意识到我们拥有访客和访客的一生,我们意识到我们可以存储状态:

+       queryCommand    {DbQueryCommandTree
|_Parameters
| |_p__linq__0 : Edm.Guid
|_Query : Collection{Record['IDGUID'=Edm.Guid, 'Name'=Edm.String, 'FooField'=Edm.String, 'BarField'=Edm.Boolean, 'Group'=Edm.String, 'IsDeleted'=Edm.Boolean]}
  |_Project
    |_Input : 'Limit1'
    | |_Limit
    |   |_Filter
    |   | |_Input : 'Extent1'
    |   | | |_Filter
    |   | |   |_Input : 'Var_8'
    |   | |   | |_Scan : CodeFirstDatabase.LeaderboardSuite
    |   | |   |_Predicate
    |   | |     |_
    |   | |       |_Var(Var_8).IsDeleted
    |   | |       |_<>
    |   | |       |_True
    |   | |_Predicate
    |   |   |_
    |   |     |_Var(Extent1).IDGUID
    |   |     |_=
    |   |     |_@p__linq__0
    |   |_2
    |_Projection
      |_NewInstance : Record['IDGUID'=Edm.Guid, 'Name'=Edm.String, 'FooField'=Edm.String, 'BarField'=Edm.Boolean, 'Group'=Edm.String, 'IsDeleted'=Edm.Boolean]
        |_Column : 'IDGUID'
        | |_Var(Limit1).IDGUID
        |_Column : 'Name'
        | |_Var(Limit1).Name
        |_Column : 'FooField'
        | |_Var(Limit1).FooField
        |_Column : 'BarField'
        | |_Var(Limit1).BarField
        |_Column : 'Group'
        | |_Var(Limit1).Group
        |_Column : 'IsDeleted'
          |_Var(Limit1).IsDeleted}  System.Data.Entity.Core.Common.CommandTrees.DbQueryCommandTree

我们也知道var newQuery = queryCommand.Query.Accept(new SoftDeleteQueryVisitor()); 节点将在Filter节点之前被命中,因为它是Scan节点的父节点。此时,我们必须考虑两种可能性:查询中没有Scan子句,查询中有where子句。

此时,我刚走过树,确定查询谓词是否在whereIsDeleted子句中使用了Equals字段,并且没有添加节点如果是这样的话。我没有费心检查NotEqualsOr条款,因为这些情况极不可能。但是,如果您发现需要它们,它们很容易添加。

Xor