Ok… predo spunto dal commento di Davide per approfondire la questione IQueryProvider e cercare di capire quali sono le difficoltà vere, o almeno quelle che ho scoperto io fino ad ora, nella realizzazione di un query provider per Linq.
Innanzitutto sottolineo che non mi sto cimentando nell’impresa, non per ora…, sto “semplicemente” cercando di capire come funziona. Come Roby sono curioso(issimo) per natura, devo smontare e rimontere tutto quello che mi passa per le mani e se non capisco mi intestardisco fino ad arrivare alla soluzione.
Vediamo innanzitutto da dove si parte. Questo snippet, preso dagli unit test, espone lo scenario:
String expected = "select [_t0].[id], [_t0].[firstName], [_t0].[lastName] from [Persons] as [_t0]";
DbRepositoryQueryProvider provider = new DbRepositoryQueryProvider();
TestPersonRepository repository = new TestPersonRepository( provider );
var query = from person in repository select person;
String actual = provider.GetQueryText( query.Expression );
Nulla di trascendentale, se non fosse che non va…, mentre questo invece funziona:
String expected = "select [_t0].[id], [_t0].[firstName], [_t0].[lastName] from [Persons] as [_t0] where [_t0].[firstName] = ‘Mauro’";
DbRepositoryQueryProvider provider = new DbRepositoryQueryProvider();
TestPersonRepository repository = new TestPersonRepository( provider );
var query = repository.Where( p => p.FirstName == "Mauro" );
String actual = provider.GetQueryText( query.Expression );
nonostante all’apparenza sia più complesso, e lo è sia ben chiaro. Ma quali sono le differenza tra il primo ed il secondo?
Tutto sta nell’expression tree che viene generato dal motore di Linq, evitando per ora gli screenshot (fidatevi), la differenza fondamentale sta che nel primo caso nell’expression tree avete una MethodCallExpression che incapsula la chiamata a “Select”, mentre nel secondo no, è implicita…
Facciamo un passo indietro:
La query Linq di partenza è decisamente, decisamente è un parolone…, semplice da convertire, il repository nell’esempio è una IQueryable<Person> e grazie al mapping, tutto hard coded sia chiaro, sappiamo che il “tipo” Person è mappato sulla tabella [Persons], allo stesso modo sappiamo che Person ha una serie di proprietà che sono mappate sulle rispettive colonne id, firstName, lastName. In questo caso nell’expression tree che viene generato troviamo, in coda, una chiamata esplicita a “Select”, quella query Linq è infatti equivalente all’expression generata dall’extension method Select():
repository.Select( p => p );
e infatti produce lo stesso identico expression tree. Ma sappiamo molto bene che in realtà la chiamata a Select() ci serve solo in 2 casi:
- L’esempio triviale di cui sopra che corrisponde ad una “select * from tableName” senza condizione, ergo vogliamo tutti i record;
- Nel momento in cui vogliamo eseguire una “projection” verso un tipo diverso da Person, e questa è tutta un’altra storia.
Normalmente possiamo scrivere:
var query = repository.Where( p => p.FirstName == "Mauro" );
e tutto funziona come deve senza esplicitare la select, cosa necessaria nella query linq per il solo fatto che è necessaria dal punto di vista del compilatore che deve capire cosa state facendo. Ma torniamo al nostro problema… e mi tocca fare un’altro passo indietro. IQueryProvider (System.Linq) non è sufficiente, nonostante sia il cuore di tutto, perchè non è l’entry point per la generazione di una “query”.
System.Linq.IQueryable<T> è il vero entry point, noi sviluppatori scriviamo query che insistono su una IQueryable<T> e solo quando invochiamo l’esecuzione, o deferred execution che sia, entra in gioco IQueryProvider. L’implementazione di IQueryable<T> è decisamente triviale ed ha il solo scopo di “conservare” l’expression tree che viene generato da chiamate consecutive a n extension methods come in questo esempio:
var query = repository.Where( p => p.FirstName == “Mauro” )
.OrderBy( p => p.LastName )
.Skip( 10 )
.Take( 5 )
.Select( p => new { DisplayName = String.Format( “{0} {1}”, p.FirstName, p.LastName ) } );
Ogni chiamata genera un nuovo expression tree, che è immutabile (importantissimo), che sta a voi memorizzare nella vostra implementazione di IQueryable<T>.
Quando andiamo in esecuzione, deferred execution in questo caso, viene effettivamente chiamato in causa il query provider:
foreach( var element in query )
{
Console.WriteLine( element );
}
A questo punto viene il bello, dato l’expression tree avete l’onere di analizzarlo e generare innanzitutto il command, gli eventuali parameters, e infine mandarlo in esecuzione recuperare u cursore forward only (IDataReader) e scorrendo i record creare (projection) le istanze delle entity.
Detta così sembra facile… in realtà il problema vero è che se stiamo scrivendo un query provider il nostro obiettivo è quello di essere general purpose e non limitare le possibilità del nostro utilizzatore. Se ci pensate la cosa è più che normale la scrittura di in query provider è paragonabile alla scrittura di un ORM e di certo quando usate NH, ad esempio, non avete limiti nelle possibilità di scrittura delle query… purtroppo infatti, lo sviluppatore, è libero di scrivere:
repository.Where( p => true );
che probabilmente non ha molto senso ma è decisamente lecito e che deve essere trasformato in una cosa del tipo:
select * from [Persons] where 1 = 1
che ancora non è detto che abbia senso ma è sicuramente lecito, ma non solo potremmo sbizzarrirci e produrre:
repository.Where( p => myMethod( p ) );
e qui sono cavoli amari perchè nell’expression tree avete una MethodCallExpression per myMethod e avete l’arduo compito di capire, ed è tutt’altro che semplice, se sia possibile eseguire quella chiamata in maniera remota, ergo su sql server ad esempio, o se invece debba essere eseguita localmente con conseguente fetch di tutti i dati e filtro in memoria… ma potremmo peggiorare:
repository.Where( p => myMethod( p ) ).OrderBy( p => p.FirstName );
mescolando ulteriormente le carte in tavola al provider che deve essere in grado di capire che la prima chiamata deve essere eseguita localmente ma la seconda può essere eseguita su sql server, deve quindi interpretare quell’expression tree come se fosse stato scritto:
repository.OrderBy( p => p.FirstName ).Where( p => myMethod( p ) );
che non so se sia lecito, ad esempio per Linq2Sql, ma rende bene l’idea.
Da questi primissimi esempi è facile capire quale sia il grado di difficoltà a cui ci si trova davanti nella scrittura di un query provider, e questo purtroppo è solo l’inizio. Vedremo, se ci saranno altre “puntate”, quali sono gli altri scogli.
.m
Technorati Tags:
Linq,
IQueryProvider