如何将expression式树转换为部分SQL查询?

当EF或LINQ to SQL运行查询时,它:

  1. 从代码构buildexpression式树,
  2. 将expression式树转换为SQL查询,
  3. 执行查询,从数据库获取原始结果并将其转换为应用程序使用的结果。

看着堆栈跟踪,我不知道第二部分发生在哪里。

一般来说,是否可以使用EF或(最好是)LINQ to SQL的一个现有部分将Expression对象转换为部分SQL查询(使用Transact-SQL语法),或者我必须重新发明轮子?


更新:评论要求提供我想要做的一个例子。

其实, 下面Ryan Wright的回答完全说明了我想要达到的结果,除了我的问题是关于如何使用EF和LINQ to SQL实际使用的.NET Framework的现有机制来实现的 ,而不必重新发明轮子,并自己写成千行的未经过testing的代码来做类似的事情。

这也是一个例子。 再一次请注意,没有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来自哪里?

通过查询Product类的Price属性的自定义DatabaseMapping属性,可以反映该名称。

名称@from@to从哪里来的预期查询?

这些名称是expression式参数的实际名称。

在预期的查询中来自哪里?

这是一个二进制expression式的可能结果。 也许EF或LINQ to SQL会代替between … and statement between … and ,而是坚持使用[Price] >= @from and [Price] <= @to 。 这也没关系,因为结果在逻辑上是相同的(我没有提到性能)并不重要。

为什么在预期的查询中没有where

因为Expression中没有指出必须有一个where关键字。 也许实际的expression式只是其中一个expression式,这些expression式之后将与二进制运算符合并,构build一个更大的查询,以便在前面添加一个where

简短的答案似乎是,你不能使用EF或LINQ到SQL的一部分作为翻译的快捷方式。 您至less需要ObjectContext一个子类来获取internal protected QueryProvider属性,这意味着创build上下文的所有开销(包括所有元数据等)。

假设你可以这样做,为了得到一个部分的SQL查询,例如,只是WHERE子句,你基本上需要查询提供程序,并调用IQueryProvider.CreateQuery()就像LINQ在其Queryable.Where实现中一样。 要获得更完整的查询,可以使用ObjectQuery.ToTraceString() 。

至于在哪里发生这种情况, LINQ提供者的基本状态一般说

IQueryProvider用LINQ框架传递的构造expression式树返回对IQueryable的引用,用于进一步调用。 一般而言,每个查询块都被转换为一堆方法调用。 对于每个方法调用,都涉及到一些expression式。 在创build我们的提供程序时(在方法IQueryProvider.CreateQuery中),我们运行expression式并填充一个filter对象,该filter对象在IQueryProvider.Execute方法中用于对数据存储库运行查询

然后

可以通过两种方式执行查询,可以通过在Query类(从IQueryableinheritance)中实现GetEnumerator方法(在IEnumerable接口中定义)来实现。 也可以直接由LINQ运行时执行

在debugging器下检查EF是前者。

如果你不想完全重新发明轮子,EF和LINQ to SQL都不是选项,那么这个系列的文章可能会有所帮助:

  • 如何:LINQ到SQL翻译
  • 如何:LINQ到SQL翻译 – 第二部分
  • 如何:LINQ to SQL Translation – 第三部分

以下是创build查询提供程序的一些源代码,可能涉及到更多繁重的工作来实现您想要的function:

  • LINQ:构build一个IQueryable提供程序系列
  • 使用LinqExtender创build自定义LINQ提供程序

是的,这是可能的,你可以使用访问者模式parsingLINQexpression式树。 您需要像下面那样通过inheritanceExpressionVisitor来构build一个查询翻译器。 通过挂接到正确的点,您可以使用翻译器从您的LINQexpression式构造您的SQLstring。 请注意,下面的代码只处理基本的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; } } 

然后通过调用访问expression式:

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

在Linq2SQL中,你可以使用:

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

你基本上必须重新发明轮子。 QueryProvider是从expression式树到存储本地语法的转换。 这就是要处理特殊情况的东西,比如string.Contains(),string.StartsWith()以及处理它的所有特殊function。 它还将处理ORM各层的元数据查找(在数据库优先或模型优先的entity framework中为* .edml)。 已经有用于构buildSQL命令的示例和框架。 但是你在找什么听起来像是一个部分的解决scheme。

还要明白,表/视图元数据是正确确定什么是合法的。 查询提供程序非常复杂,除了将简单的expression式树转换为SQL以外,还为您做了大量的工作。

为了回应你的第二部分在哪里发生。 第二部分在枚举IQueryable的时候发生。 IQueryables也是IEnumerables,最终当GetEnumerator被调用时,它将依次调用查询提供程序与将使用其元数据生成sql命令的expression式树。 这不正是发生了什么,但它应该得到的想法。

您可以使用下面的代码:

 var query = from c in Customers select c; string sql = ((ObjectQuery)query).ToTraceString(); 

查看以下信息: 检索由Entity Provider生成的SQL 。

这不是完整的,但是如果你晚点来的话,这里有一些想法可供借鉴:

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

不知道这是否正是你所需要的,但它看起来可能是closures的:

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