c# - كيفية تحويل شجرة تعبير إلى استعلام SQL جزئي؟



3 Answers

نعم ، يمكنك ذلك ، يمكنك تحليل شجرة تعبير LINQ باستخدام نمط الزائر. ستحتاج إلى إنشاء مترجم استعلام عن طريق تصنيف فئة فرعية ExpressionVisitor مثل أدناه. من خلال ربط النقاط الصحيحة ، يمكنك استخدام المترجم لإنشاء سلسلة SQL الخاصة بك من تعبير LINQ الخاص بك. لاحظ أن الشفرة أدناه تتعامل فقط مع العبارات الأساسية / orderby / skip / take ، ولكن يمكنك ملؤها مع المزيد حسب الحاجة. نأمل أن يكون بمثابة خطوة أولى جيدة.

public class MyQueryTranslator : ExpressionVisitor
{
    private StringBuilder sb;
    private string _orderBy = string.Empty;
    private int? _skip = null;
    private int? _take = null;
    private string _whereClause = string.Empty;

    public int? Skip
    {
        get
        {
            return _skip;
        }
    }

    public int? Take
    {
        get
        {
            return _take;
        }
    }

    public string OrderBy
    {
        get
        {
            return _orderBy;
        }
    }

    public string WhereClause
    {
        get
        {
            return _whereClause;
        }
    }

    public MyQueryTranslator()
    {
    }

    public string Translate(Expression expression)
    {
        this.sb = new StringBuilder();
        this.Visit(expression);
        _whereClause = this.sb.ToString();
        return _whereClause;
    }

    private static Expression StripQuotes(Expression e)
    {
        while (e.NodeType == ExpressionType.Quote)
        {
            e = ((UnaryExpression)e).Operand;
        }
        return e;
    }

    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.DeclaringType == typeof(Queryable) && m.Method.Name == "Where")
        {
            this.Visit(m.Arguments[0]);
            LambdaExpression lambda = (LambdaExpression)StripQuotes(m.Arguments[1]);
            this.Visit(lambda.Body);
            return m;
        }
        else if (m.Method.Name == "Take")
        {
            if (this.ParseTakeExpression(m))
            {
                Expression nextExpression = m.Arguments[0];
                return this.Visit(nextExpression);
            }
        }
        else if (m.Method.Name == "Skip")
        {
            if (this.ParseSkipExpression(m))
            {
                Expression nextExpression = m.Arguments[0];
                return this.Visit(nextExpression);
            }
        }
        else if (m.Method.Name == "OrderBy")
        {
            if (this.ParseOrderByExpression(m, "ASC"))
            {
                Expression nextExpression = m.Arguments[0];
                return this.Visit(nextExpression);
            }
        }
        else if (m.Method.Name == "OrderByDescending")
        {
            if (this.ParseOrderByExpression(m, "DESC"))
            {
                Expression nextExpression = m.Arguments[0];
                return this.Visit(nextExpression);
            }
        }

        throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
    }

    protected override Expression VisitUnary(UnaryExpression u)
    {
        switch (u.NodeType)
        {
            case ExpressionType.Not:
                sb.Append(" NOT ");
                this.Visit(u.Operand);
                break;
            case ExpressionType.Convert:
                this.Visit(u.Operand);
                break;
            default:
                throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
        }
        return u;
    }


    /// <summary>
    /// 
    /// </summary>
    /// <param name="b"></param>
    /// <returns></returns>
    protected override Expression VisitBinary(BinaryExpression b)
    {
        sb.Append("(");
        this.Visit(b.Left);

        switch (b.NodeType)
        {
            case ExpressionType.And:
                sb.Append(" AND ");
                break;

            case ExpressionType.AndAlso:
                sb.Append(" AND ");
                break;

            case ExpressionType.Or:
                sb.Append(" OR ");
                break;

            case ExpressionType.OrElse:
                sb.Append(" OR ");
                break;

            case ExpressionType.Equal:
                if (IsNullConstant(b.Right))
                {
                    sb.Append(" IS ");
                }
                else
                {
                    sb.Append(" = ");
                }
                break;

            case ExpressionType.NotEqual:
                if (IsNullConstant(b.Right))
                {
                    sb.Append(" IS NOT ");
                }
                else
                {
                    sb.Append(" <> ");
                }
                break;

            case ExpressionType.LessThan:
                sb.Append(" < ");
                break;

            case ExpressionType.LessThanOrEqual:
                sb.Append(" <= ");
                break;

            case ExpressionType.GreaterThan:
                sb.Append(" > ");
                break;

            case ExpressionType.GreaterThanOrEqual:
                sb.Append(" >= ");
                break;

            default:
                throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));

        }

        this.Visit(b.Right);
        sb.Append(")");
        return b;
    }

    protected override Expression VisitConstant(ConstantExpression c)
    {
        IQueryable q = c.Value as IQueryable;

        if (q == null && c.Value == null)
        {
            sb.Append("NULL");
        }
        else if (q == null)
        {
            switch (Type.GetTypeCode(c.Value.GetType()))
            {
                case TypeCode.Boolean:
                    sb.Append(((bool)c.Value) ? 1 : 0);
                    break;

                case TypeCode.String:
                    sb.Append("'");
                    sb.Append(c.Value);
                    sb.Append("'");
                    break;

                case TypeCode.DateTime:
                    sb.Append("'");
                    sb.Append(c.Value);
                    sb.Append("'");
                    break;

                case TypeCode.Object:
                    throw new NotSupportedException(string.Format("The constant for '{0}' is not supported", c.Value));

                default:
                    sb.Append(c.Value);
                    break;
            }
        }

        return c;
    }

    protected override Expression VisitMember(MemberExpression m)
    {
        if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter)
        {
            sb.Append(m.Member.Name);
            return m;
        }

        throw new NotSupportedException(string.Format("The member '{0}' is not supported", m.Member.Name));
    }

    protected bool IsNullConstant(Expression exp)
    {
        return (exp.NodeType == ExpressionType.Constant && ((ConstantExpression)exp).Value == null);
    }

    private bool ParseOrderByExpression(MethodCallExpression expression, string order)
    {
        UnaryExpression unary = (UnaryExpression)expression.Arguments[1];
        LambdaExpression lambdaExpression = (LambdaExpression)unary.Operand;

        lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);

        MemberExpression body = lambdaExpression.Body as MemberExpression;
        if (body != null)
        {
            if (string.IsNullOrEmpty(_orderBy))
            {
                _orderBy = string.Format("{0} {1}", body.Member.Name, order);
            }
            else
            {
                _orderBy = string.Format("{0}, {1} {2}", _orderBy, body.Member.Name, order);
            }

            return true;
        }

        return false;
    }

    private bool ParseTakeExpression(MethodCallExpression expression)
    {
        ConstantExpression sizeExpression = (ConstantExpression)expression.Arguments[1];

        int size;
        if (int.TryParse(sizeExpression.Value.ToString(), out size))
        {
            _take = size;
            return true;
        }

        return false;
    }

    private bool ParseSkipExpression(MethodCallExpression expression)
    {
        ConstantExpression sizeExpression = (ConstantExpression)expression.Arguments[1];

        int size;
        if (int.TryParse(sizeExpression.Value.ToString(), out size))
        {
            _skip = size;
            return true;
        }

        return false;
    }
}

ثم قم بزيارة التعبير عن طريق الاتصال:

var translator = new MyQueryTranslator();
string whereClause = translator.Translate(expression);
c# .net linq entity-framework lambda

عندما يقوم EF أو LINQ إلى SQL بتشغيل استعلام ، فإنه:

  1. يقوم ببناء شجرة تعبير من الكود ،
  2. يحول شجرة التعبير إلى استعلام SQL ،
  3. ينفذ الاستعلام ، ويحصل على النتائج الأولية من قاعدة البيانات ويحولها إلى النتيجة ليتم استخدامها من قبل التطبيق.

بالنظر إلى أثر المكدس ، لا أستطيع معرفة مكان حدوث الجزء الثاني.

بشكل عام ، هل من الممكن استخدام جزء موجود من EF أو (يفضل) LINQ إلى SQL لتحويل كائن Expression إلى استعلام SQL جزئي (باستخدام بناء جملة Transact-SQL) ، أو يجب إعادة اختراع العجلة؟

التحديث: يطلب التعليق تقديم مثال لما أحاول القيام به.

في الواقع ، يوضح إجابة رايان رايت أدناه تمامًا ما أريد تحقيقه كنتيجة ، باستثناء حقيقة أن سؤالي يتعلق تحديدًا بكيفية القيام بذلك عن طريق استخدام آليات موجودة لـ .NET Framework يتم استخدامها فعليًا بواسطة EF و LINQ إلى SQL ، بدلاً من الاضطرار إلى إعادة اختراع العجلة وكتابة آلاف الأسطر من الشفرة التي لم يتم اختبارها بنفسى للقيام بنفس الشيء.

هنا أيضا مثال. مرة أخرى ، لاحظ أنه لا يوجد رمز تم إنشاؤه بواسطة ORM.

private class Product
{
    [DatabaseMapping("ProductId")]
    public int Id { get; set; }

    [DatabaseMapping("Price")]
    public int PriceInCents { get; set; }
}

private string Convert(Expression expression)
{
    // Some magic calls to .NET Framework code happen here.
    // [...]
}

private void TestConvert()
{
    Expression<Func<Product, int, int, bool>> inPriceRange =
        (Product product, int from, int to) =>
            product.PriceInCents >= from && product.PriceInCents <= to;

    string actualQueryPart = this.Convert(inPriceRange);

    Assert.AreEqual("[Price] between @from and @to", actualQueryPart);
}

من أين يأتي اسم Price في الاستعلام المتوقع؟

يمكن الحصول على الاسم من خلال الانعكاس عن طريق الاستعلام عن السمة DatabaseMapping المخصصة لخاصية Price لفئة Product .

من أين تأتيfrom و @to في الاستعلام المتوقع؟

هذه الأسماء هي الأسماء الفعلية لمعلمات التعبير.

من أين تأتي between … and وتأتي من الاستعلام المتوقع؟

هذه نتيجة محتملة للتعبير الثنائي. ربما EF أو LINQ إلى SQL ، بدلاً من between … and [Price] >= @from and [Price] <= @to ، تتمسك بـ [Price] >= @from and [Price] <= @to بدلاً من ذلك. لا بأس أيضا ، لا يهم حقا لأن النتيجة منطقية هي نفسها (أنا لا أذكر الأداء).

لماذا لا يوجد where في الاستعلام المتوقع؟

لأنه لا يوجد شيء يشير في Expression أنه يجب أن تكون هناك كلمة رئيسية في where . ربما يكون التعبير الفعلي هو أحد التعبيرات التي سيتم دمجها فيما بعد مع المشغلين الثنائيين لإنشاء استعلام أكبر للإحاطة المسبق where .




لم يكتمل الأمر ، ولكن إليك بعض الأفكار التي تود طرحها إذا كنت قد وصلت إليها لاحقًا:

    private string CreateWhereClause(Expression<Func<T, bool>> predicate)
    {
        StringBuilder p = new StringBuilder(predicate.Body.ToString());
        var pName = predicate.Parameters.First();
        p.Replace(pName.Name + ".", "");
        p.Replace("==", "=");
        p.Replace("AndAlso", "and");
        p.Replace("OrElse", "or");
        p.Replace("\"", "\'");
        return p.ToString();
    }

    private string AddWhereToSelectCommand(Expression<Func<T, bool>> predicate, int maxCount = 0)
    {           
        string command = string.Format("{0} where {1}", CreateSelectCommand(maxCount), CreateWhereClause(predicate));
        return command;
    }

    private string CreateSelectCommand(int maxCount = 0)
    {
        string selectMax = maxCount > 0 ? "TOP " + maxCount.ToString() + " * " : "*";
        string command = string.Format("Select {0} from {1}", selectMax, _tableName);
        return command;
    }








Related