web-dev-qa-db-fra.com

Comment convertir une arborescence d'expression en une requête SQL partielle?

Lorsque EF ou LINQ to SQL exécute une requête, il:

  1. Construit une arborescence d'expression à partir du code,
  2. Convertit l'arborescence d'expressions en une requête SQL,
  3. Exécute la requête, obtient les résultats bruts de la base de données et les convertit en résultat à utiliser par l'application.

En regardant la trace de la pile, je ne peux pas comprendre où se passe la deuxième partie.

En général, est-il possible d'utiliser une partie existante d'EF ou (de préférence) LINQ to SQL pour convertir un objet Expression en une requête SQL partielle (en utilisant la syntaxe Transact-SQL), ou je dois réinventer la roue ?


Mise à jour: un commentaire demande de fournir un exemple de ce que j'essaie de faire.

En fait, la réponse de Ryan Wright ci-dessous illustre parfaitement ce que je veux réaliser, sauf le fait que ma question concerne spécifiquement comment puis-je le faire en utilisant les mécanismes existants de. NET Framework réellement utilisé par EF et LINQ to SQL, au lieu d'avoir à réinventer la roue et écrire moi-même des milliers de lignes de code non testé pour faire la même chose.

Voici également un exemple. Encore une fois, notez qu'il n'y a pas de code généré par 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);
}

D'où vient le nom Price dans la requête attendue?

Le nom peut être obtenu par réflexion en interrogeant l'attribut DatabaseMapping personnalisé de la propriété Price de la classe Product.

Où viennent les noms @from et @to provenir de la requête attendue?

Ces noms sont les noms réels des paramètres de l'expression.

Où va between … and provenir de la requête attendue?

Ceci est le résultat possible d'une expression binaire. Peut-être que EF ou LINQ to SQL le feraient, au lieu de between … and déclaration, respectez [Price] >= @from and [Price] <= @to au lieu. C'est ok aussi, ça n'a pas vraiment d'importance puisque le résultat est logiquement le même (je ne parle pas des performances).

Pourquoi n'y a-t-il pas where dans la requête attendue?

Parce que rien n'indique dans le Expression qu'il doit y avoir un mot clé where. Peut-être que l'expression réelle n'est qu'une des expressions qui seront combinées plus tard avec des opérateurs binaires pour construire une plus grande requête à ajouter avec where.

42
Arseni Mourzenko

La réponse courte semble être que vous ne pouvez pas utiliser une partie d'EF ou LINQ to SQL comme raccourci vers la traduction. Vous avez besoin d'au moins une sous-classe de ObjectContext pour accéder au internal protectedQueryProvider propriété, ce qui signifie tous les frais généraux de création du contexte, y compris toutes les métadonnées, etc.

En supposant que vous êtes d'accord avec cela, pour obtenir une requête SQL partielle, par exemple, juste la clause WHERE, vous allez essentiellement avoir besoin du fournisseur de requête et appeler IQueryProvider.CreateQuery () tout comme LINQ le fait dans son implémentation de Queryable.Where . Pour obtenir une requête plus complète, vous pouvez utiliser ObjectQuery.ToTraceString () .

Quant à savoir où cela se produit, principes de base du fournisseur LINQ indique généralement que

IQueryProvider renvoie une référence à IQueryable avec l'arborescence d'expression construite transmise par le framework LINQ, qui est utilisé pour d'autres appels. En termes généraux, chaque bloc de requête est converti en un groupe d'appels de méthode. Pour chaque appel de méthode, certaines expressions sont impliquées. Lors de la création de notre fournisseur - dans la méthode IQueryProvider.CreateQuery - nous parcourons les expressions et remplissons un objet filtre, qui est utilisé dans la méthode IQueryProvider.Execute pour exécuter une requête sur le magasin de données

et cela

la requête peut être exécutée de deux manières, soit en implémentant la méthode GetEnumerator (définie dans l'interface IEnumerable) dans la classe Query, (qui hérite d'IQueryable); ou il peut être exécuté directement par le runtime LINQ

Vérifier EF sous le débogueur, c'est le premier.

Si vous ne voulez pas réinventer complètement la roue et que ni EF ni LINQ to SQL ne sont des options, cette série d'articles pourrait peut-être vous aider:

Voici quelques sources pour créer un fournisseur de requêtes qui impliquent probablement beaucoup plus de travail de votre part pour implémenter ce que vous voulez:

23
Kit

Oui, c'est possible, vous pouvez analyser une arborescence d'expression LINQ en utilisant le modèle de visiteur. Vous devez construire un traducteur de requête en sous-classant ExpressionVisitor comme ci-dessous. En vous connectant aux bons points, vous pouvez utiliser le traducteur pour construire votre chaîne SQL à partir de votre expression LINQ. Notez que le code ci-dessous ne traite que des clauses basiques where/orderby/skip/take, mais vous pouvez en remplir plus si nécessaire. Espérons que ce soit une bonne première étape.

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

Ensuite, visitez l'expression en appelant:

var translator = new MyQueryTranslator();
string whereClause = translator.Translate(expression);
42
Ryan Wright

Ce n'est pas complet, mais voici quelques réflexions à approfondir si vous venez par la suite:

    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
MarkWalls

Dans Linq2SQL, vous pouvez utiliser:

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

Vous devez essentiellement réinventer la roue. Le QueryProvider est la chose qui fait la traduction des arborescences d'expression en sa syntaxe native de magasin. C'est la chose qui va gérer des situations spéciales comme string.Contains (), string.StartsWith () et toutes les fonctions spéciales qui le gèrent. Il va également gérer les recherches de métadonnées dans les différentes couches de votre ORM (* .edml dans le cas de Entity Framework d'abord sur la base de données ou sur le modèle). Il existe déjà des exemples et des cadres pour construire des commandes SQL. Mais ce que vous recherchez ressemble à une solution partielle.

Comprenez également que les métadonnées de table/vue sont nécessaires pour déterminer correctement ce qui est légal. Les fournisseurs de requêtes sont assez complexes et font beaucoup de travail pour vous au-delà de la simple conversion d'arbres d'expression en SQL.

En réponse à votre situation, où se passe la deuxième partie. La deuxième partie se produit lors de l'énumération de l'IQueryable. IQueryables sont également IEnumerables et, finalement, lorsque GetEnumerator est appelé, il va à son tour appeler le fournisseur de requête avec l'arborescence d'expression qui va utiliser ses métadonnées pour produire une commande sql. Ce n'est pas exactement ce qui se passe, mais cela devrait faire passer l'idée.

4
Orion Adrian

Vous pouvez utiliser le code suivant:

var query = from c in Customers
            select c;

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

Jetez un œil aux informations suivantes: Récupération du SQL généré par le fournisseur d'entité .

3
Wouter de Kort

Je ne sais pas si c'est exactement ce dont vous avez besoin, mais il semble que ce soit proche:

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);
0
James Johnson