[PL] Własny dostawca w Linq (dla opornych) – cz. 4


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

Pobieranie i zwrot danych


W poprzednich częściach omówiliśmy sobie podstawy konstrukcji naszego własnego dostawcy do Linq. Powinnyśmy już wiedzieć, że filary opierają się na implementacji dwóch interfejsów IQueryable oraz IQueryProvider. Ich implementacja rozkłada się w zasadzie na dwie części. Jedna to odpowiednia interpretacja dostarczonego już drzewa wyrażeń pochodzącego z zapytania Linq. Pomysł jak je poprawnie zanalizować i przetworzyć, np. na zapytanie SQL (w ciągu znaków) dałem w poprzedniej części.

Jeżeli założę, że ten etap mamy zrobiony to do kompletu nie pozostaje już nic innego jak poprawny zwrot danych zgodny z oczekiwanym formatem. Tym zagadnieniem zajmę się w tej części.

Do tej pory stworzyliśmy parę klas i ich metod, które miały generyczny parametr. Ten parametr naogół wiązał się typem zwracanego obiektu. W drugiej części tego artykułu przedstawiłem wam tymczasowy fragment kodu:

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


W żaden sposób nie spełnia on warunku, aby zwrócić generyczny typ o jaki go poprosimy w naszym "jakimkolwiek" zapytaniu. W omawianym przykładzie był to Customer, wcale przecież tak nie musi być. Ba.. nawet jeśli operujemy na typie Customer i nasze proste zapytanie:



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



Zaczniemy badać pod kątem dostępnych metod rozszerzających to jaki typ zwrotny oczekują od generycznych metod z IQueryProvider::Execute() to się okaże, że:
* result.First(); powinien nam zwrócić typ Customer
* result.Count(); powinien nam zwrócić typ Int32;
* result.Any(); powinen nam zwrócić typ bool, i słusznie bo to nic innego jak


{ return result.Count > 0 ? true : false; }


Zatem gdybym miał naszą metodę getResult zaimplementować naprawdę generycznie trzeba się chwilkę zastanowić. Ja w swoim przypadku posłużym się dalej swoją na razie wciąż tymczasowo zaprojektowaną klasą QueryExecutionPlan. Wygląda sobie ona mniej więcej tak:


    public class QueryExecutionPlan
    {        
        public QueryExecutionPlan()
        {...
        public string QueryString;
//gotowy SQL do wykorzystania, np. w SqlCommand
        public QueryResultTypes QueryResultType;
//typ zwracanego rekordu
        public QueryAggregateType AggregateType;
//typ agregatu jeśli występuje
        public String AggregateKeyColumn;
//kluczowa kolumna przy agregatach 

        public Type ReturnType;
//podstawowy typ zwracanego rekordu
        public Type QueryType;
//konkretny typ generycznego Query<T>

        public bool Validate();
//sprawdzanie poprawności stworzonego planu
    }

QueryResultTypes oraz QueryAggregateType to enumeratory na ten moment wyglądające tak:


    public enum QueryResultTypes
    {
        Resultset, //enumerator
        SingleRecord, //pojedyncza instancja rekordu
        Aggregate //wartości numeryczne, np: max,min,count,sum, itd

        IfEmpty //test czy rezultat zwraca jakikolwiek wynik
    }

    public enum QueryAggregateType
    {
        Count, Sum, Min, Max
    }


Idea jest prosta. W przypadku naszego powyższego zapytania Linq, oczekuję w QueryString = "select c.* from Customers c where c.Age>10". W przypadku wykonania funkcji Any, Count, Max, Min oczekuje, że odpowiednie dodatkowe parametry zostaną przekazane w odpowiednich właściwościach. Mój QueryTranslator z drzewa wyrażeń powinien mi to dostarczyć i wtedy odczytywanie rekordów mógę zacząć od następującego ruchu:

Zmieniam trochę swoją klasę dostawcy, teraz jest to abstrakcyjna podstawowa klasa BaseQueryProvider:


    public abstract class BaseQueryProvider : IQueryProvider
    {
        public IQueryable<T> CreateQuery<T>(Expression expression)
        {…}


        public IQueryable CreateQuery(Expression expression)
        {…}


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


        public object Execute(Expression expression)
        {
            QueryExecutionPlan execPlan = null;
            execPlan = Translate(expression);
            return RetrieveResults(execPlan);
        }


        protected abstract QueryExecutionPlan Translate(
              Expression expression);
        protected abstract object RetrieveResults(QueryExecutionPlan plan);

    }

Implementacja CreateQuery się nie zmieniła, a w Execute drobna kosmetyka w stosunku do tego co pokazałem wcześniej. Na podstawie tej bazowej klasy zrobiłem sobie właściwego dostawce, który jak już wspomniałem wcześniej docelowo ma służyć do obsługi wielu fizycznych typów źródeł danych. Zatem mamy MultiSourceQueryProvider:


   public class MultiSourceQueryProvider : BaseQueryProvider
    {
        public MultiSourceConfiguration Configuration
        {
            get;
            set;       
        }


        public MultiSourceQueryProvider(MultiSourceConfiguration config)
        {
            this.Configuration = config;
        }


        public override QueryExecutionPlan Translate(Expression expression)
        {
            switch (Configuration.Type)
            {
                case MultiSourceType.SQLServer:                    
                    return translateToSQLServer(expression);
                case MultiSourceType.Oracle:
                    return translateToOracle(expression);
                case MultiSourceType.File:
                    return translateToFile(expression);
            }

            throw new NotImplementedException("No valid source type provided!");
       

        protected override object RetrieveResults(QueryExecutionPlan plan)
        {
            switch (Configuration.Type)
            {
                case MultiSourceType.SQLServer:
                    return retrieveFromSQLServer(plan);
                case MultiSourceType.Oracle:
                    return retrieveFromOracle(plan);
                case MultiSourceType.File:
                    return retrieveFromFile(plan);
            }

            throw new NotImplementedException("Cannot retrieve data from this source!”);
        } 
    }

Jak widać powyższa klasa nie jest kompletna. Brakuje definicji konfiguracji oraz implementacji poszczególnych metod translateTo* oraz retrieveFrom*
O tym za moment, to na co warto zwrócić uwagę to fakt, że trochę sztucznie ograniczyłem możliwość dalszego rozszerzania tej klasy. Jeśli za moment wpadlibyśmy na moment, że chcemy też wspierać mySQL czy cokolwiek innego to trzeba się dogrzebać do tej klasy i rozbudować switch/case w obu przypadkach. Może lepiej by było dodać dwa typy obiektów - BaseTranslator oraz BaseRetriever, które tutaj były by przekazywane w liście, mogłby by być dodawane nawet w runtime'ie jako pluginy, cokolwiek. Jest to już rozważanie czysto architektoniczne, na razie jako taki zalążek do wieloźródłowego dostawcy Linq mam.

Co dalej? W przypadku translate sprawa jest prosta, już mamy naszego QueryTranslator<T>, który dostarcza nam odpowiednio spreparowany plan wykonania (omówione w cz. 3). Teraz ostatni element, dla przykładu retrieveFromSQLServer().

To co napewno musimy mieć to połączenie do bazy. Tutaj nie ucieknę od starego dobrego Ado.Net:


            IDataReader dataReader = null;
           
SqlConnection conn = new SqlConnection(Configuration.ConfigurationString);
            SqlCommand cmd = new SqlCommand(plan.QueryString,conn);
            conn.Open();
            dataReader = cmd.ExecuteReader();


no i mamy IDataReader. Teraz bardzo chętnie bym sprawdził czy dane i struktura schowana w tym readerze jest zgodna z oczekiwanym typem zwrotnym. Zatem chwila na refleksję :). Oczywiści chodzi mi o System.Reflection. W przypadku gdy mamy zwrócić jeden rekord typu Customer (np. resultset.First()) sprawa wyglądała by mniej więcej tak:

Mamy System.Type naszego Customer ukryte w QueryExecutionPlan.ReturnType;
W tym typie mamy zaszyta informację na temat publicznych pól jakie zawiera poprzez:


FieldInfo[] fields = type.GetFields();


W FieldInfo mamy informacje o nazwie i typie każdego z nich. Zatem na początek sprawdziłbym zgodność pomiędzy tą tablicą a tą zawartą w readerze ala:


            for (int i = 0; i < reader.FieldCount; i++)
            {
                 … reader.GetName(i);
                 … reader.GetFieldType(i);

            }

Mając już pewność, że mamy po obustronnach zgodność, że żadne "mandatory" pole potrzebne do stworzenia obiektu nie zaginęło w zwróconym wyniku możemy sobie spokojnie skonstruować taki obiekcik. Całość w osobnej metodzie zamknąłem w takim kodzie:

        public override IEnumerable ProcessInputData()

        { 
           
bool ok = ValidateSourceAndDestination(reader);
            FieldInfo[] fields = plan.ReturnType.GetFields();

            if (ok)
            {
                while (reader.Read())
                {
                    object instance =
                       Activator.CreateInstance(plan.ReturnType);
                    for (int i = 0; i < reader.FieldCount; i++)
                    {
                        FieldInfo fi =
                            (from f in fields
                            where f.Name.ToLower()
                                 == reader.GetName(i).ToLower()
                            select f).First();
                        fi.SetValue(instance, reader.GetValue(i));
                    }
                    yield return instance;
                }
            }
         }


No i mamy niby komplet. Teraz powyższy zlepek idei i kodu należy zebrać w jeden projekt, wypieścić, skompilować i mamy własnego dostawcę do Linq.

Pytanie tylko czyżby? Zakładam, że już w pełni interpretujemy drzewo wyrażeń i doskonale zoptymalizowaliśmy nasze zapytanie. Przekazaliśmy je do naszych fizycznych źródeł danych i zwracamy zgodnie z oczekiwanym formatem to ciągle mi brakuje jednej, kolejnej dużej rzeczy, która ściśle łaczy się z sensem wykorzystania ORM - pracy na metamodelach.

W tym czterech częściach praktycznie w ogóle o nim nie wspomniałem, a jest moim zdaniem następnym solidnym krokiem w zabawie.

Podsumowując: Nie dałem wam konkretnego gotowego i działającego przykładu, ale wydaje mi się, że dostarczyłem gotowego pomysłu jak takowego dostawcę stworzyć. Pozostaje jeszcze dużo niewiadomych na dalszych etapach, do których zapewne w następnych wpisach wrócę. Jednym z nich jest jak wspomniałem metamodel. Inne tematy to kwestia zapisu i transakcji, która w powyższych rozważaniach też nie została zupełnie uchwycona.
Co na temat Linq w trójwarstwówce i "plucie" zapytaniami do serwera aplikacyjnego?
To jak już akumulujemy pewne informacje na takowym serwerze to może warto pomyśleć o cache'u zapytań. Może warto dodać do tego workflow już na etapie konstrukcji metamodeli?

Wierzę, że tematów i zagadnień można sobie mnożyć. Osobiście nie wierzę, że ten temat da się w jakikolwiek sposób wyczerpać, więc aż się dziwie, że nie ma jeszcze żadnych komentarzy. 🙂

Skip to main content