Как преобразовать дерево выражений в частичный SQL-запрос? - PullRequest
41 голосов
/ 12 октября 2011

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

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

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

В общем, возможно ли использовать существующую часть 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.

Ответы [ 7 ]

42 голосов
/ 25 октября 2011

Да, это возможно, вы можете проанализировать дерево выражений LINQ, используя шаблон посетителя.Вам необходимо создать переводчик запросов, создав подкласс ExpressionVisitor, как показано ниже.Подбирая правильные точки, вы можете использовать переводчик для построения строки SQL из выражения LINQ.Обратите внимание, что приведенный ниже код имеет дело только с основными пунктами where / 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);
22 голосов
/ 28 октября 2011

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

Предполагая, что вы согласны с этим, чтобы получить частичный 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 не являются опциями, возможно, эта серия статей поможет:

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

5 голосов
/ 28 апреля 2016

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

    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;
    }
5 голосов
/ 12 октября 2011

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

var cmd = DataContext.GetCommand(expression);
var sqlQuery = cmd.CommandText;
4 голосов
/ 25 октября 2011

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

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

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

2 голосов
/ 25 октября 2011

Вы можете использовать следующий код:

var query = from c in Customers
            select c;

string sql = ((ObjectQuery)query).ToTraceString();

Просмотрите следующую информацию: Получение SQL-кода, сгенерированного провайдером сущностей .

0 голосов
/ 28 октября 2011

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

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's 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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...