Подтвердить что ты не робот

Как преобразовать дерево выражений в частичный запрос SQL?

Когда EF или LINQ to SQL запускает запрос, он:

  • Создает дерево выражений из кода,
  • Преобразует дерево выражений в запрос SQL,
  • Выполняет запрос, получает необработанные результаты из базы данных и преобразует их в результат, который будет использоваться приложением.

Посмотрев на трассировку стека, я не могу понять, где происходит вторая часть.

В общем, можно ли использовать существующую часть EF или (желательно) LINQ to SQL для преобразования объекта Expression в частичный SQL-запрос (с использованием синтаксиса Transact-SQL), или я должен изобретать колесо


Обновление: комментарий просит предоставить пример того, что я пытаюсь сделать.

На самом деле, ответ Райана Райт ниже отлично иллюстрирует то, что я хочу достичь в результате, за исключением того, что мой вопрос конкретно о том, как я могу это сделать используя существующие механизмы .NET Framework, которые фактически используются EF и LINQ to 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 to SQL вместо оператора between … and следует придерживаться [Price] >= @from and [Price] <= @to. Это тоже нормально, это не имеет большого значения, поскольку результат логически одинаковый (я не упоминаю о производительности).

Почему в ожидаемом запросе нет where?

Потому что ничто не указывает в Expression, что должно быть ключевое слово where. Возможно, фактическое выражение является лишь одним из выражений, которые позже будут объединены с бинарными операторами, чтобы построить более крупный запрос для добавления с помощью where.

4b9b3361

Ответ 1

Короткий ответ кажется, что вы не можете использовать часть EF или LINQ to SQL как ярлык для перевода. Для получения свойства internal protected QueryProvider вам нужен хотя бы подкласс ObjectContext, а это означает все накладные расходы на создание контекста, включая все метаданные и т.д.

Предполагая, что вы в порядке с этим, чтобы получить частичный запрос SQL, например, только предложение WHERE, в котором вам в основном понадобится поставщик запроса, и вызовите IQueryProvider. CreateQuery() так же, как LINQ делает в своей реализации Queryable.Where. Чтобы получить более полный запрос, вы можете использовать ObjectQuery.ToTraceString().

Что касается того, где это происходит, основы LINQ-провайдера, обычно говорится, что

IQueryProvider возвращает ссылку на IQueryable с построенным деревом выражений, переданным базой LINQ, которая используется для дальнейших вызовов. В общем, каждый блок запроса преобразуется в кучу вызовов методов. Для каждого вызова метода присутствуют некоторые выражения. При создании нашего поставщика - в методе IQueryProvider.CreateQuery - мы запускаем выражения и заполняем объект фильтра, который используется в методе IQueryProvider.Execute для запуска запроса в хранилище данных

и что

запрос может быть выполнен двумя способами, либо путем реализации метода GetEnumerator (определенного в интерфейсе IEnumerable) в классе Query (который наследуется от IQueryable); или он может быть выполнен непосредственно в режиме выполнения LINQ

Проверка EF под отладчиком - первая.

Если вы не хотите полностью изобретать колесо, и ни EF, ни LINQ to SQL не являются параметрами, возможно, эта серия статей поможет:

Вот некоторые источники для создания поставщика запросов, которые, вероятно, связаны с гораздо более тяжелым подъемом с вашей стороны, чтобы реализовать то, что вы хотите:

Ответ 2

Да, возможно, вы можете проанализировать дерево выражений 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);

Ответ 3

В Linq2SQL вы можете использовать:

var cmd = DataContext.GetCommand(expression);
var sqlQuery = cmd.CommandText;

Ответ 4

Вы в основном должны изобретать колесо. QueryProvider - это то, что делает перевод с деревьев выражений на него, сохраняя собственный синтаксис. Это то, что будет обрабатывать особые ситуации, а также string.Contains(), string.StartsWith() и все специализированные функции, которые обрабатывают его. Он также будет обрабатывать поиск метаданных в различных слоях вашего ORM (*.edml в случае с базой данных или первой Entity Framework). Уже есть примеры и рамки для построения команд SQL. Но то, что вы ищете, похоже на частичное решение.

Также понимайте, что метаданные таблицы/представления необходимы для правильного определения того, что является законным. Поставщики запросов довольно сложны и выполняют большую работу за вас, кроме преобразования простых преобразований в SQL.

В ответ на ваш вопрос, где происходит вторая часть. Вторая часть происходит во время перечисления IQueryable. IQueryables также являются IEnumerables, и в конечном итоге, когда GetEnumerator называется, он, в свою очередь, будет вызывать поставщик запросов с деревом выражений, который будет использовать свои метаданные для создания команды sql. Это не совсем то, что происходит, но оно должно получить эту идею.

Ответ 6

Это не полно, но вот некоторые мысли для вас, чтобы вы начинали, если вы придете к этому позже:

    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;
    }

Ответ 7

Не уверен, что это именно то, что вам нужно, но похоже, что он может быть близок:

string[] companies = { "Consolidated Messenger", "Alpine Ski House", "Southridge Video", "City Power & Light",
                   "Coho Winery", "Wide World Importers", "Graphic Design Institute", "Adventure Works",
                   "Humongous Insurance", "Woodgrove Bank", "Margie Travel", "Northwind Traders",
                   "Blue Yonder Airlines", "Trey Research", "The Phone Company",
                   "Wingtip Toys", "Lucerne Publishing", "Fourth Coffee" };

// The IQueryable data to query.
IQueryable<String> queryableData = companies.AsQueryable<string>();

// Compose the expression tree that represents the parameter to the predicate.
ParameterExpression pe = Expression.Parameter(typeof(string), "company");

// ***** Where(company => (company.ToLower() == "coho winery" || company.Length > 16)) *****
// Create an expression tree that represents the expression 'company.ToLower() == "coho winery"'.
Expression left = Expression.Call(pe, typeof(string).GetMethod("ToLower", System.Type.EmptyTypes));
Expression right = Expression.Constant("coho winery");
Expression e1 = Expression.Equal(left, right);

// Create an expression tree that represents the expression 'company.Length > 16'.
left = Expression.Property(pe, typeof(string).GetProperty("Length"));
right = Expression.Constant(16, typeof(int));
Expression e2 = Expression.GreaterThan(left, right);

// Combine the expression trees to create an expression tree that represents the
// expression '(company.ToLower() == "coho winery" || company.Length > 16)'.
Expression predicateBody = Expression.OrElse(e1, e2);

// Create an expression tree that represents the expression
// 'queryableData.Where(company => (company.ToLower() == "coho winery" || company.Length > 16))'
MethodCallExpression whereCallExpression = Expression.Call(
    typeof(Queryable),
    "Where",
    new Type[] { queryableData.ElementType },
    queryableData.Expression,
    Expression.Lambda<Func<string, bool>>(predicateBody, new ParameterExpression[] { pe }));
// ***** End Where *****

// ***** OrderBy(company => company) *****
// Create an expression tree that represents the expression
// 'whereCallExpression.OrderBy(company => company)'
MethodCallExpression orderByCallExpression = Expression.Call(
    typeof(Queryable),
    "OrderBy",
    new Type[] { queryableData.ElementType, queryableData.ElementType },
    whereCallExpression,
    Expression.Lambda<Func<string, string>>(pe, new ParameterExpression[] { pe }));
// ***** End OrderBy *****

// Create an executable query from the expression tree.
IQueryable<string> results = queryableData.Provider.CreateQuery<string>(orderByCallExpression);

// Enumerate the results.
foreach (string company in results)
    Console.WriteLine(company);