[PL] Własny dostawca w LINQ (dla opornych) – cz. 2


Artykuł został rozbity na wiele części, poniżej spis treści:
I.  Budowa zapytania
II. Podstawy analizy drzewa wyrażeń
III. Właściwa analiza drzewa wyrażeń
IV. Pobieranie i zwrot danych

Podstawy analizy drzewa wyrażeń

Kontynuując zabawę w własnego dostawcę do Linq chciałem rozwinąć kwestię analizy drzewa wyrażeń i tłumaczenia go na konkretne polecenia (np. SQL). W poprzedniej części wspomniałem o jednej z metod interfejsu IQueryProvider, czyli o BasicQueryProvider.Execute oraz BasicQueryProvider.Execute<T> w naszej implementacji dostawcy.

Stworzony już prosty test rozbudujemy o próbę realnego wykonania zapytania, chociażby poprzez odwołanie do pierwszego znalezionego rekordu:


      Query<Customer> q = new Query<Customer>();



var result = (from c in q
      where c.Age > 10
     
select c).First();
Console.WriteLine(result.ToString());


Śledząc wywołania w debugerze (lub rozpoczętą praktyką wyjątków) odkryjemy, że następnymi metodami do poprawnej implementacji są właśnie dwie odmiany Execute().
Dla ułatwienia na ten moment wersję z szablonem zrobimy tak: 

public TRes Execute<TRes>(Expression expression)
{
     return (TRes)this.Execute(expression);
}


I skupimy się na tej drugiej. Nim ją jednak rozdmucham przyjrzyjmy się parametrowi, czyli klasie Expression.  Obiekt tej klasy wydaje się mieć niewiele właściwości jak na drzewo wyrażeń:

    public abstract class Expression
 
   {
        protected Expression(ExpressionType nodeType, Type type);
        public ExpressionType NodeType { get; }
        public Type Type { get; }

//dużo statycznych metod, o których za moment


}

Definicja klasy zawiera o wiele więcej elementów statycznych pomocnych w konstrukcji pełnego drzewa. Nie będę wymieniał wszystkich (parę kliknięć w Visual Studio zrobi to za mnie lepiej) dla przykładu tylko nadmienię parę tak, aby pokazać wam, czego można się spodziewać:

public static BinaryExpression Add(Expression left, Expression right);
public static BinaryExpression And(Expression left, Expression right);
public static MemberAssignment Bind(MemberInfo member, Expression expression);
public static MethodCallExpression Call(Expression instance, MethodInfo method);


Bez tłumaczenia za bardzo, o co chodzi w powyższym można się domyśleć, że z pełną pulą tych metod można sobie spokojnie zagnieździć całkiem skomplikowane drzewo wyrażeń reprezentujące nasze zapytanie. Problem tylko gdzie ta informacja jest skoro obiekt klasy Expression posiada tak niewiele.
Odpowiedź pewnie sami widzicie poniekąd w tym, co powyższe metody zwracają – wyspecjalizowane typy dziedziczące po bazowym Expression. Powyżej mamy BinaryExpression i MethodCallExpression jako przykłady.


Pełne drzewko wygląda tak (źródło: MSDN):

System..::.Object
  System.Linq.Expressions..::.Expression
   
System.Linq.Expressions..::.BinaryExpression
   
System.Linq.Expressions..::.ConditionalExpression
   
System.Linq.Expressions..::.ConstantExpression
   
System.Linq.Expressions..::.InvocationExpression
   
System.Linq.Expressions..::.LambdaExpression
   
System.Linq.Expressions..::.MemberExpression
   
System.Linq.Expressions..::.MethodCallExpression
   
System.Linq.Expressions..::.NewExpression
   
System.Linq.Expressions..::.NewArrayExpression
   
System.Linq.Expressions..::.MemberInitExpression
   
System.Linq.Expressions..::.ListInitExpression
   
System.Linq.Expressions..::.ParameterExpression
   
System.Linq.Expressions..::.TypeBinaryExpression
   
System.Linq.Expressions..::.UnaryExpression

Typów wyrażeń jak widać jest całkiem sporo. Gdybyśmy w naszym Execute sprawdzili, jaki faktyczny typ Expression siedzi w opakowaniu, to na pewno byłby to któryś z powyższych wyspecjalizowanych obiektów.
Zatem spróbujmy przygotować szablon do dalszych testów. Do istniejącej bazy kodu dodałem dwie klasy:


   internal class QueryExecutionPlan
    {
        public QueryExecutionPlan()
        {
            QueryString = "";
            First = false;
            IsValid = false;
        }
        public string QueryString;
        public bool First;
        public bool IsValid;
    }

Ta klasa ma być wynikiem naszego przetwarzania i tłumaczenia drzewa na konkretne zapytanie SQL. Z planem wykonania na razie poza nazwą niewiele tę klasę łączy, ale później będzie to pewien koncept do dalszego rozwoju. Druga klasa, jaką stworzyłem to:

    internal class QueryTranslator<T>
    {
        QueryExecutionPlan queryExecPlan;
        public QueryTranslator()
        { … }

        public QueryExecutionPlan Translate(Expression expression)
        {
            queryExecPlan = new QueryExecutionPlan();             
            renderQueryString(expression);
            return queryExecPlan;
        }

        private string renderQueryString(Expression expression) {
            if (expression==null)
                return "";

            switch (expression.NodeType)
            {
                case ExpressionType.Call:
                      //do implementacji
                    break;
                case ExpressionType.Constant:
                      //do implementacji
                    break;
                case ExpressionType.Quote:
                      //do implementacji
                      break;

default:
//dla wszystkich typów, których z jakiegoś powodu nie chcemy wspierać
    throw new NotImplementedException(

@"QueryExecutionPlan does not
        support used type of expression: "
      

expression.NodeType.ToString());


            }
            return "";
        }
    }

Tutaj na moment się zatrzymam w celu objaśnienia kolejnej nowości – ExpressionType.
Ten numerator zawarty w Expression.NodeType jest kluczowy, jeśli chcemy określić, jaki typ wyrażenia siedzi w środku przekazanego parametru. Powyżej podałem parę nieobsłużonych jeszcze przykładów czyli:
ExpressionType.Call - dla wszelkich funkcji jak na przykład: Join(), Where(), First()
ExpressionType.Constant – dla stałych zwracanych i/lub przekazywanych
ExpressionType.Quote – dla parametrów kontekstowych

Ich jest o wiele więcej i do pary z wszystkimi podtypami Expression omówię je dokładniej w dalszej części. Powyższego switcha na pewno w najbliższej kolejności będziemy mocno rozbudowywać, aby nasze drzewo wyrażeń przekształcić w coś bardziej użytecznego.

Egzekutorem powyższych jest oczywiście IQueryProvider::Execute. W naszym wykonaniu na ten moment wygląda to mniej więcej tak:


   public class BasicQueryProvider : IQueryProvider
    {
        //reszta bez zmian 


       
public object Execute(Expression expression)
        {
            string rawQueryString = ""; 
            rawQueryString = Translate(expression);
            return getResult(rawQueryString);            
        } 
        public string Translate(Expression expression)
        {
            QueryExecutionPlan plan =
                new QueryTranslator<Customer>().Translate(expression);
            return plan.QueryString;
        } 
        protected object getResult(string sqlQuery)
        {
            Customer cust = new Customer(); 
            //tu jeszcze nic nie robimy
            //Customer ze względu na bieżacy stan przykładu
            //jest tutaj hardcode'owanym typem zwrotnym 
            return cust;
        }
    }

Aby zachować dobry styl Linq to SQL dodałem jeszcze do Query poniższy kod:

        public override string ToString()
        {
            return provider.Translate(this.Expression);
        }

Dzięki temu przed wykonaniem zawsze w stringu będę mógł sprawdzić, jakie zapytanie stworzyłem poprzez:


var
result = (from c in q
      where c.Age > 10
            select c);
Console.WriteLine(result.ToString());

Zwracany result jest w końcu typu Query<Customer>. Na razie zatrzymam się w tym momencie.
W następnej części dokładnie omówię każde wyspecjalizowane *Expression oraz ExpressionType tak, aby powyższy switch nabrał realnego sensu.

Dla zniecierpliwionych dam dobry start do samodzielnej zabawy w postaci:


switch (expression.NodeType)
{
case ExpressionType.Call:                   
 
MethodCallExpression callExp =
         (MethodCallExpression)expression;
 
if (callExp.Method.DeclaringType.Name == "Queryable")
 
       {
         switch (callExp.Method.Name)
          {
             case "Where":
              
queryExecPlan.QueryString = 
              "SELECT [RESULT] FROM [BODY] WHERE [CONDITIONS] [POSTPARAMS]";
               string body = renderQueryString(callExp.Arguments[0]);
              
string conditions = renderQueryString(callExp.Arguments[1]);

              
queryExecPlan.QueryString =
                  queryExecPlan.QueryString.Replace(
                                "[CONDITIONS]", conditions);
              
queryExecPlan.QueryString = 
                  queryExecPlan.QueryString.Replace("[BODY]", body);
             break;
             default:
 
              throw new NotImplementedException(
                             @"QueryExecutionPlan does not support 
                              used type of CALL method: " +
                              callExp.Method.Name);
           }
       

       break;
       default:
            
throw new NotImplementedException(
          "QueryExecutionPlan does not support used type of expression: " +
             expression.NodeType.ToString());
}


To chyba dobra podstawa, aby zacząć budować realne zapytanie. Taki stan switcha potraktuję jako podstawę w następnych rozważaniach.

Podsumowując drugą część i podstawy analizy drzewa wyrażeń po uważnej lekturze powinniście zapamiętać:
1) Sama konstrukcja zapytania LINQ w naszym kodzie nie powoduje jeszcze wykonania zapytania. Dopiero odwołanie się do jakiejkolwiek funkcji zwracającej realne rekordy powoduje wykonanie metody IQueryProvider::Execute i zarazem zwrócenie odpowiedniego wyniku.
2) Dla przejrzystości naszego kodu i planu wykonania zapytania (czy w postaci komendy SQL czy jakiejkolwiek własnej interpretacji) warto rozdzielić sam proces tłumaczenia zapytania oraz jego wykonanie i zwrócenie wyników. Może się to przydać w dalszych etapach, np. przy optymalizacji zapytań.
3) Podstawowym typem reprezentującym drzewo wyrażeń jest obiekt typu Expression. W rzeczywistości w środku na ogół siedzi bardziej wyspecjalizowana wersja odpowiadająca za każdy poszczególny element zapytania. Całe drzewo oczywiście badamy iteracyjnie uzależniając od konkretnego typu wyrażenia.
4) Aby dobrze zinterpretować typ wyrażenia należy badać Expression.NodeType, każda z wartości powinna mieć odpowiednie odzwierciedlenie w wyspecjalizowanych typach dziedziczących po Expression


Skip to main content