web-dev-qa-db-ja.com

式ツリーを部分的なSQLクエリに変換する方法は?

EFまたはLINQ to SQLがクエリを実行すると、次のことが行われます。

  1. コードから式ツリーを構築し、
  2. 式ツリーをSQLクエリに変換します。
  3. クエリを実行し、データベースから生の結果を取得し、アプリケーションで使用される結果に変換します。

スタックトレースを見ると、2番目の部分がどこで発生するかわかりません。

一般に、EFの既存の部分または(できれば)LINQ to SQLを使用してExpressionオブジェクトを部分的なSQLクエリに変換することは可能ですか(Transact-SQL構文を使用)、またはホイールを再発明する必要があります?


更新:コメントは、私がやろうとしていることの例を提供するよう求めています。

実際、 以下のRyan Wrightの答え は、結果として達成したいことを完全に示していますが、私の質問が具体的にであるという事実を除いて、既存の.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を付加するより大きなクエリを作成する式の1つにすぎません。

42

短い答えは、翻訳のショートカットとしてEFまたはLINQ to SQLのpartを使用できないということです。 internal protectedQueryProvider プロパティを取得するには、少なくともObjectContextのサブクラスが必要です。これは、すべてのメタデータなどを含む、コンテキスト作成のすべてのオーバーヘッドを意味します。

それでよければ、たとえば、部分的なSQLクエリを取得するには、基本的にWHERE句だけを取得するには、クエリプロバイダーを必要とし、 IQueryProvider.CreateQuery() を呼び出します。 LINQが Queryable.Where の実装で行うように。より完全なクエリを取得するには、 ObjectQuery.ToTraceString() を使用できます。

これが発生する場所については、 LINQプロバイダーの基本 は一般的に

IQueryProviderは、LINQフレームワークから渡された構築済みの式ツリーを使用してIQueryableへの参照を返します。これは、以降の呼び出しに使用されます。一般的に、各クエリブロックはメソッド呼び出しの束に変換されます。各メソッド呼び出しには、いくつかの式が含まれます。メソッドIQueryProvider.CreateQueryでプロバイダーを作成している間、式を実行し、データストアに対してクエリを実行するためにIQueryProvider.Executeメソッドで使用されるフィルターオブジェクトを埋めます

そしてそれ

クエリは、Queryクラス(IQueryableから継承)でGetEnumeratorメソッド(IEnumerableインターフェイスで定義)を実装することにより、2つの方法で実行できます。または、LINQランタイムによって直接実行できます

デバッガーでEFをチェックするのは前者です。

ホイールを完全に再発明したくなく、EFもLINQ to SQLもオプションではない場合は、おそらく次の一連の記事が役立ちます。

クエリプロバイダーを作成するためのいくつかのソースを次に示します。これは、おそらく、あなたが望むものを実装するためにあなたの側により多くの負担をかける必要があります。

23
Kit

はい、可能です。訪問者パターンを使用してLINQ式ツリーを解析できます。以下のようにExpressionVisitorをサブクラス化して、クエリトランスレーターを構築する必要があります。正しいポイントにフックすることにより、トランスレーターを使用してLINQ式からSQL文字列を構築できます。以下のコードは、基本的な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);
42
Ryan Wright

それは完全ではありませんが、後でこれを手に入れた場合にグルーブするためのいくつかの考えがあります:

    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

Linq2SQLでは、次を使用できます。

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

基本的に、車輪を再発明する必要があります。 QueryProviderは、式ツリーからストアのネイティブ構文への変換を行うものです。これは、string.Contains()、string.StartsWith()、およびそれを処理するすべての特殊機能と同様に、特別な状況を処理するものです。また、ORMのさまざまなレイヤー(データベース優先またはモデル優先のEntity Frameworkの場合は* .edml)でメタデータ検索を処理します。 SQLコマンドを作成するための例とフレームワークはすでにあります。しかし、探しているものは部分的な解決策のように聞こえます。

また、何が合法であるかを正しく判断するには、テーブル/ビューのメタデータが必要であることも理解してください。クエリプロバイダーは非常に複雑であり、単純な式ツリーをSQLに変換する以外にも多くの作業を行います。

2番目の部分はどこで発生しますか。 2番目の部分は、IQueryableの列挙中に発生します。 IQueryablesもIEnumerablesであり、最終的にGetEnumeratorが呼び出されると、メタデータを使用してsqlコマンドを生成する式ツリーでクエリプロバイダーを呼び出します。それは正確に何が起こるかではありませんが、それは全体にアイデアを得る必要があります。

4
Orion Adrian

次のコードを使用できます。

var query = from c in Customers
            select c;

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

次の情報をご覧ください。 エンティティプロバイダーによって生成されたSQLの取得

3
Wouter de Kort

これが本当に必要なものかどうかはわかりませんが、近いかもしれません:

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