[PL] Własny dostawca w LINQ (dla opornych) - cz. 3

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

Właściwa analiza drzewa wyrażeń

W poprzedniej części wprowadziłem temat drzewa wyrażeń zwracanego poprzez uniwersalny typ Expression. Wierzę jednak, że zawarte tam informacje poprzez ich chwilową niekompletność mogą wprowadzić więcej zamieszania niż wyjaśnienia. Ta część powinna umożliwić wam napisanie translatora uwzględniającego chociażby najprostsze scenariusze wykorzystania zapytań Linq.

Nasze status quo na ten moment to stworzona klasa QueryTranslator, której zadaniem jest przetworzenie drzewa wyrażeń na ciąg znaków zgodny z językiem SQL. Aby ułatwić zabawę poprzednio omówioną wersję trochę zrefaktoryzowałem (uwielbiam te kalki językowe J). Aktualny jej stan to:

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

        public QueryExecutionPlan Translate(Expression expression)
        {
            queryExecPlan = new QueryExecutionPlan();
          queryExecPlan.ReturnType = typeof(T);
            queryExecPlan.QueryType = typeof(Query<T>);           

            Expression returnExpression =expression;
            do
            {
                returnExpression = analyzeExpressionTree(returnExpression);
            } while (returnExpression != null);
            return queryExecPlan;
        }

        private Expression analyzeExpressionTree(Expression expression)
        {
     if (expression == null)
                return null;
            Expression ret = null;
            switch (expression.NodeType)
            {
                case ExpressionType.Add:
                case ExpressionType.AddChecked:
                case ExpressionType.And:

case WSZYSTKIE_ExpressionType:
  …
                case ExpressionType.Subtract:
                case ExpressionType.SubtractChecked:
                case ExpressionType.TypeAs:
                case ExpressionType.TypeIs:
                case ExpressionType.UnaryPlus:
                    //tu ma być realny kod analizujący wyrażenie
                    break;

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

    }

Aby nie wydłużać powyższego fragmentu kodu wyciąłem komplet przypadków do switcha, pozostawiając tylko parę, aby pokazać wam szerszy obraz. W realnym kodzie są wszystkie i wszystkie w ten czy inny sposób musimy uwzględnić. Patrząc po ExpressionType możemy zobaczyć wszystkie rodzaje zapytań jakie wspiera składnia Linq.

W poprzedniej części obiecałem przyjrzeć się dokładniej, co ExpressionType zawiera i jakie to ma odniesienie do wszelakich klas pochodnych od Expression. Pełny opis wszystkich wyliczeń tego typu jest dostępny na MSDN:
https://msdn.microsoft.com/en-us/library/bb361179.aspx
Konfrontując ich liczbę z liczbą klas dziedziczących po Expression widać, że przy analizie wypadałoby zrobić pewną agregację. Tak dla przykładu patrząc po opisie UnaryExpression na MSDN, ten typ wyrażenia zwracany jest w przypadku ExpressionType o wartościach:
Negate, NegateChecked, Not, Convert, ConvertChecked, ArrayLength, Quote, TypeAs. Przeglądając wszystkie typy Expression i wartości numeratora, jakie wspierają warto powyższego switcha pogrupować tak, aby odpowiednie typy ExpressionType były obsługiwane przez jednego wyspacjelizowanego case’a.  Gotowy przykład klasy, która odpowiednio analizuje takie drzewo jest dostępny czy na blogu Matt’a Warrena, czy też w artykule na Code guru napisanym przez Jacka Matulewskiego. Klasa ExpressionVisitor opisana tam zawiera dosyć znaczącą liczbę niezbyt przejrzystego kodu, więc aby umożliwić sobie lekkie wprowadzenie do poprawnej analizy drzewa wyrażeń zachęcam na początek przy prostych przykładach zapytań implementować poszczególne typy ExpressionType jakie lądują, np. w powyższej metodzie analyzeExpressionTree. W przypadku naszego prostego zapytania:

                var result = (from c in Customers
                              where c.Age > 10
                              select c);

drzewo powinno wyglądać następująco:

image 
Luźno z takiego drzewa można spróbować złożyć „select * from Customers c where c.Age>10”. Gdybyśmy do naszego kodu Linq dodali First(). To zanim wystąpił by nasz Call(Where) wystąpiłby Call(First). W SQL przełożyć to możemy na „select top 1 * from Customers c where c.Age>10”

Gdybym nasze zapytanie zmodyfikował tak, aby nie zwracać całego Customer:

                var result = (from c in Customers
                              where c.Age > 10
                select c.FirstName).First();

Wtedy ścieżka wyglądała by tak:
1) Call (First)
2) Call (Select)
3) Call (Where)
.. a dalej już jak na powyższym rysunku.

Poprawna interpretacja MethodCallExpression dla metody „Select” powinna nas doprowadzić do oczywistej interpretacji czyli: „select c.FirstName from customers c where c.Age>10”

Przekładanie powyższego drzewa na ciąg znaków z kodem SQL najlepiej przetwarzać na początku przez StringBuilder (tak jak to ma miejsce w przypadku ExpressionVisitor).
Ja powyżej dałem początek własnemu obiektowi przejściowemu QueryExecutionPlan, który ma mi być pomocny przy przekładaniu mojego Linq na dedykowana składnię SQL dla SQL Servera (T-SQL), Oracle'a (PL-SQL) czy też zestaw poleceń do wykonania dla własnej prostej bazy plikowej. Tutaj trochę zdradzam finał, który mam nadzieję niedługo opublikować czyli taki dostawca do Linq, który poprzez prostą konfigurację umożliwia komunikację z różnym fizycznym źródłem danych (poprzez wspólny metamodel).
Czyli krótko mówiąc idziemy powoli po bebechach w stronę EntityFramework (aloha Syzyfie, wymyślmy kolejne koło :>)

Jakby, ktoś chciał sobie przyspieszyć to jak wspomniałem przykłady alternatywne przykłady są dostępne w sieci. Na początku jednak polecam taką lekką drogę przez mekę aby zobaczyć co kryje się w pierwszym Expression jakie przekazujemy i jak dalej to wygląda gdy zagłebimy się w osadzone podwyrażenia.
To się może przydać przy optymalizacji generowanych zapytań jako odpowiedź dla co poniektórych, którzy ze składni LINQ są zadowoleni, ale może nie do końca z generowanych przez nie SQLi. UnitTesting przy takim projekcie moim zdaniem to jedno wielkie MUST-BE.

Podsumowując: jeśli założymy, że powyższe już mamy zrobione to jedyne co pozostaje to wykonać polecenie, odebrać wynik i zformatować go zgodnie z typem jaki zarządaliśmy w zapytaniu Linq. To już ponownie pozwolę sobie przesunąć na kolejną część.

Technorati Tagi: Polish posts,coding,Linq,c#,.NET Framework