Expression Trees - 2. daļa
Turpinot tēmu par izteiksmju kokiem, pirmā daļā apskatīju nelielu ievadu šajā tehnoloģijā un apskatīju pāris pamatprincipus, kas tad īsti ir izteiksmju koki.
No pirmā acu uzmetiena izskatās, ka izteiksmju koki paredzēti tikai vaicājuma izteiksmes (lambda izteiksme) pārvēršanai datos, lai no tiem varētu tranformēt pieprasījumu kādā pavisam citā tehnoloģijā (piemēram, labmda izteiksmes -> SQL vaicājums). Tomēr nedaudz apskatot tehnoloģiju tuvāk var ieraudzīt nelielu potenciālu vaicājumu vai filtrācijas mehānisma izveidei tieši izpildes laikā (runtime).
Noteikti, ka sistēmās redzēti šāda veida ekrāni:
Šāda veida izteiksmes var kontruēt arī ar izteiksmju koku palīdzību. Pirmo betu mēģināsim uzkontruēt šoreiz.
Pirmais, kas mums ir nepieciešams ir datu avots kuru vēlamies filtrēt. Šim nolūkam var izmantot gan objektu kolekcijas (IEnumerable<T> interfeiss), gan arī datus no kāda cita datu avota, kas realizē IQueryable<T> interfeisu.
Kā datu avotu izmantosim SQL datu bāzi ar ļoti vienkāršu modeli, kas satur fiktīvu klientu datus:
Pēc tam kad datu avots uz izveidots, varam ķerties pie filtrācijas metodes izveides. Paraksts metodei, kas veiks datu avota filtrāciju būs šāds:
public T[] FilterSource<T>(IEnumerable<T> list)
Metode saņems sarakstu ar elementiem, kas nāk no datu avots (IEnumerable<T> interfeiss, par cik IQueryable<T> mantojas no šī interfeisa, tad objektorientētā programmēšana mums nodrošina arī pieeju IQueryable<T> interfeisam šādā metodes parakstā).
Iesākumā jāizdomā kāds būs vaicājuma koks, kas ir jāģenerē (pirmajā betā diemžēl tiek apskatītas tikai vienlīmeņa vaicājumi bez sarežģītākām izteiksmēm).
Konstruējamais izteiksmes koks ir šāds:
- iesākumā mums visa izteiksme ir jāparametrizē jeb jānodefiē, kas ir nākošais parametrs izteiksme un kam tiks piemērots tālākās filtrācijas izteiksmes. Šim nolūkam izmantosim Expression.Parameter;
- pēc tam ir jāizveido izteiksmes ķermenis (expression body), kas varbūt dažāda veida bināras izteiksmes (=, !=, >, <, utt.). Šim nolūkam izmantosim, piemēram, Expression.Equal metodi;
- lai sekmīgi izveidotu izteiksmes ķermeni, binārajai operācijai ir nepieciešama labās un kreisās puses izteiksmes;
- kreisā puse ir ienākošā objekta īpašības izteiksme (izteiksme, kas izsauc objekta īpašību - property). Šim nolūkam izmantosim Expression.Property metodi;
- labā puse parasti ir konstante, ko ievada lietotājs filtrācijas veidošanas ekrānā (pagaidām, šī programmas versija piedāvā tikai filtrāciju pēc iepriekš ievadītas konstantes, vairāk advancētus topikus domājams aprakstīt nākamajos rakstos par izteiksmju kokiem). Šim nolūkam izmantosim Expression.Constant metodi;
- pēc tam izveidoto izteikmsi varam iekļaut lambda izteiksmē ar Expression.Lambda metodes palīdzību;
Jāņem vērā ar vēl daži konvertācijas un tipu nesakritības problēmas, bet tās visas iznesām ārā uz palīdzības metodēm atvieglojot pašu filtrācijas izteiksmes veidošanas kodu. Tātad kopējās kods izskatās šāds:
var incoming = Expression.Parameter(typeof (T), "c");
var propertyExpression = Expression.Property(incoming, property);
var propertyInfo = (PropertyInfo)propertyExpression.Member;
var pType = propertyInfo.PropertyType;
var constant = CreateConstantExpr(exprValue, pType);
Expression expr = null;
switch (op)
{
case "=":
expr = Expression.Equal(propertyExpression, constant);
break;
case "LIKE":
// special case - LIKE operation (we need to invoke `Contains(string)' method on target property if it's type of string)
if (pType == typeof(string))
{
expr = Expression.Call(propertyExpression, "Contains", null, constant);
}
else
{
expr = Expression.Equal(propertyExpression, constant);
}
break;
case "!=":
expr = Expression.NotEqual(propertyExpression, constant);
break;
case ">":
expr = Expression.GreaterThan(propertyExpression, constant);
break;
case ">=":
expr = Expression.GreaterThanOrEqual(propertyExpression, constant);
break;
case "<":
expr = Expression.LessThan(propertyExpression, constant);
break;
case "<=":
expr = Expression.LessThanOrEqual(propertyExpression, constant);
break;
}
var lambda = Expression.Lambda(expr, incoming);
Tātad koda loģika ir šāda:
- izveidojam ienākošā parametra izteiksmi;
- izveidojam īpašības piekļūšanas izteiksmi;
- izveidojam konstantes izteiksmi atkarībā no mērķa objekta īpašības datu tipa;
- pēc izvēlētā operatora konstruējam pirmā līmeņa izteiksmes ķermeni;
- izveidoto izteiksmi ietveram labmda izteikmsē, kuru parametrizējam ar ienākošo mērķa objektu;
Tālāk jaunizveidotā lambda izteiksme būtu jāpielieto datu avota datu filtrācijai. Ja aplūkojam IEnumerable<T> un IQueryable<T> interfeisu Where() metodes, tad ieraugām nelielu atšķirību:
public static System.Linq.IQueryable<TSource> Where<TSource>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<Func<TSource,bool>> predicate)
public static System.Collections.Generic.IEnumerable<TSource> Where<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Func<TSource,bool> predicate)
Atšķirība ir tāda, ka IEnumerable<T> interfeiss sagaida jau sagatavotu Func<TSource, bool> objektu (delegātu), kuru pielieto katram no kolekcijas elementiem, lai veiktu nepieciešamo filtrāciju datu kopai.
Turpretīm, IQueryable<T> interfeiss sagaida Expression<Func<TSource, bool>> objektu, kas ir izteiksmes objekts, kuru vēlāk izmanto System.Linq.IQueryProvider Provider { get; } objekts, lai konvertētu funkcijas delegātu uz izteiksmes koku, kuru izmanto nepieciešamo tranformāciju veikšanai.
Lai gan pēc metodes paraksta ir redzama atšķirība, tomēr LINQ to SQL un Entity Framework (šie ir datu avotu, kas tiek pielietoti aplikācijā) dzinēji pielieto arī norādīto Func delegātu, ja ar datu avotu tiek strādāts kā ar IEnumerable<T> interfeisu, kas patīkami saīsina kodu.
Tātad, lai iegūtu Func<TSource, bool> delegātu no lambda izteiksmes, nepieciešams to vienkārši nokompilēt konvertējot to uz delegātu. Ģenerētās lambda izteikmes pielietošana datu avota filtrācijai ir sekojoša:
var func = (Expression<Func<T, bool>>)lambda;
return list.Where(func.Compile()).ToArray();
Tas arī viss pagaidām.
Zemāk ir redzami pāris ekrāna šāviņi no programmas darbības:
Šajā programmas versijā ir zināmi jau pāris mīnusi. To realizācija ir paredzēta nākamjās versijās:
- Viena līmeņa hierarhija: izteiksmes pagaidām nodrošina tikai viena līmeņa izteiksmes, piemēram, nav iespējas veidot izteiksmes: kur `vārds' LIKE 'Jān%' UN `dzimšanas-datums' >= 1974-04-23
- Mērķa datu avota objektiem ir pieejams tikai pirmā līmeņa īpašības, t.i., nevar veidot pagaidām vaicājumus kā, piemēram, `klients'.`adrese'.`iela' = 'Brīvības'
- Pagaidām iespējama tikai salīdzināšana ar iepriekš ievadītu konstanti, piemēram, nav iespējams veidot vaicājumu `klients'.`vārds' != `klients'.`uzvārds'
Cerams, ka šis raksts deva lielāku ieskatu izteiksmju kokos un reālāku pielietojumu informācijas sistēmās. Jāatzimē, protams, ka pirmā versija programmai nav diezko bagāta ar iespējām, bet tas ir tikai laika jautājums :) Nākāmajā rakstā mēģināsim norealizēt iztrūkstošo funckionalitāti un apskatīsim vēl advancētākus topikus ar vēl reālāku ikdienas pielietojumu biznesa informācijas sistēmās.
Cerams, ka noderēs!