Prestazioni delle query LINQ in architetture complesse

di Cristian Civera, in .NET 4.0,

In un post precedente ho parlato dell'interfaccia IQueryable e dell'utilità che può avere se passata tra i vari strati dell'applicativo, dal repository fino ai servizi applicativi. La tecnica è molto comoda ed elegante, ma comporta alcuni problemi, uno dei quali è la scelta delle navigation property da materializzare, che per l'appunto ho risolto con una tecnica illustrata nel post menzionato.

Nell'esperienza da me fatta mi sono però un giorno ritrovato con una query dalle prestazioni davvero scadenti. La query in questione impiegava 6 secondi per ottenere una sola entità con molteplici navigation property da caricare perciò subito ho dato la colpa a SQL Server. Guardo la query TSQL con SQL Profiler, la eseguo e noto con sorpresa che su SQL Sever essa viene eseguita in un istante, perciò ripiego su Entity Framework che, sebbene i dati non siano tanti, impiega molto tempo nell'analisi della view e nella preparazione della query. Faccio quindi un test compilando la query con la classe CompiledQuery, come da best practice, e ottengo così prestazioni ottime. Il problema è che in una soluzione a strati, le query compilate non si possono usare, perché solo chi consuma i dati conosce la query managed e la dovrebbe compilare, ma così facendo si accoppierebbe al tipo di repository. La prima soluzione che ho pensato è di creare metodi specifici sull'interfaccia di repository, tipo "DammiOrdineConDettaglioECliente", perdendo però i benefici di IQueryable e creando operazioni ripetitive. Ho quindi cercato un'altra strada riprendendo la stessa tecnica dell'extension method include.

Sempre nel post precedente ho mostrato come possiamo metterci in mezzo, tra la query ed Entity Framework, per manipolarla prima che venga passata al motore. Ho applicato quindi lo stesso concetto intervenendo anche in questo caso sull'interfaccia IQueryProvider. Ipotizzando di avere una query tipo:

int n = 1;
customers.Where(c => c.Orders.Count > n)

l'idea è quella di intervenire prima di eseguirla e di:

  • renderla generica in modo da essere indipendente da variabili e parametri;
  • guardare in cache se è presente il delegate oppure compilare la query;
  • invocare la funzione passando le variabili e i parametri.

Per il primo passaggio uso un expression visitor anche in questo caso che trasforma la query precedente in questa:

Convert(Param_0.Customers).Where(c => (c.Orders.Count > Param_1))

Come si vede, la query è stata parametrizzata per accettare l'ObjectContext e il valore di confronto che è esterno alla query (la variabile n). A questo punto uso il ToString dell'expressione stessa, come chiave per fare lookup su un dizionario di chiave/delegate. Se non lo trovo uso la classe CompiledQuery, individuo il metodo adatto per il numero di parametri generici da passare, compilo la query e salvo il delegate nel dizionario. Per farvi capire, di seguito il codice che uso per la compilazione:

public Delegate Compile(IEnumerable<ParameterExpression> parameters, Type returnType)
{
  // Preparo i parametri della lambda mettendo in coda il tipo ritornato
  Type[] types = parameters.Select(p => p.Type).Concat(new Type[] { returnType }).ToArray();

  // Ottengo il tipo di delegate
  Type funcType = Type.GetType(String.Format("System.Func`{0}, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", types.Length));
  if (funcType == null)
    funcType = Type.GetType(String.Format("System.Func`{0}, System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", types.Length), true);
  funcType = funcType.MakeGenericType(types);

  // Creo la lambda in base al tipo
  var lambda = Expression.Lambda(funcType, expression, parameters);

  // Chiamo il metodo compile specifico
  MethodInfo method = typeof(CompiledQuery).GetMethods(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)
    .FirstOrDefault(n => n.Name == "Compile" && n.IsGenericMethod && n.GetGenericArguments().Length == types.Length);
  Delegate compiledFunction = (Delegate)method.MakeGenericMethod(types).Invoke(null, new object[] { lambda });

  return compiledFunction;
}

A questo punto devo invocare il delegate, perciò riprendo i parametri della query corrente da eseguire e li passo al delegate, ottenendo così il risultato.

Così facendo metto dell'overhead per la traduzione della query, ma i benefici che ottengo dalla compilazione sono ben superiori, il tutto trasparente ai vari strati. Sto preparando una libreria che contenga queste estensioni, supporti Code first e permette di pluggare manipulatore ed esecutori della query; presto la metterò online.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese