[EF] QueryCompilate e MergeOption

Per ottenere il massimo delle prestazioni quando si devono usare LINQ query è consigliato precompilarle e quando possibile usare anche l’opzione MergeOption.NoTracking. Quest’ultima evita di salvare le informazioni sulle modifiche degli oggetti nell’ObjectContext. E cosi’ ho fatto; ho compilate le query e le ho usate per leggere delle entità e modificarle. Ma ad un certo punto saltava fuori un errore sulla SaveChanges che non poteva essere eseguita poichè la query era stata compilata con l’opzione MergeOption.NoTracking.

Vediamo cosa era successo. Consideriamo il seguente codice che compila una query che ritorna un utente data la email. Poi ci sono due metodi che usano questa query; una che legge solamente il nome dell’utente e quindi viene preceduto dal NoTracking ed un secondo metodo che scrive il nome dell’utente e quindi implicitamente è preceduta da AppendOnly, il valore di default di MergeOption .

public class QueryCompiled 
{
  public static Func<MyEntities, string, IQueryable<Users>> GetUserByEmail = 
            CompiledQuery.Compile<MyEntities, string, IQueryable<Users>>( 
                                             (MyEntities ctx, string email) => 
                                             from a in ctx.Users 
                                             where a.Email == email 
                                             select a);
}

public class A
{
  public string GetUserName(string email)
  {
    using (MyEntities context = new MyEntities()) 
    {
      context.Users.MergeOption = MergeOption.NoTracking; 
      var users= (QueryCompiled.GetUserByEmail(context, email)); 
      return users.Name;
    }
  }

  public void SetUserName(string email, string name)
  {
    using (MyEntities context = new MyEntities()) 
    {
      var users= (QueryCompiled.GetUserByEmail(context, email)); 
      users.Name = name;
      context.SaveChanges();
    }
  }
}

Se l’ordine di esecuzione dei metodi della classe A è il seguente va tutto bene :

A classeA = new A();
classeA.SetUserName(“aaa@aaa.aa”, “pippo”);
classeA.GetUserName(“aaa@aaa.aa”);

Se l’ordine di esecuzione si inverte, come nel seguente caso, allora ottengo l’errore che dicevo prima :

A classeA = new A();
classeA.GetUserName(“aaa@aaa.aa”);
classeA.SetUserName(“aaa@aaa.aa”, “pippo”);

Il motivo è il seguente; quando la query compilata viene invocata la prima volta allora viene memorizzata in una chache una forma ottimizzata della query. Durante questa prima invocazione vengono memorizzate altre informazioni tra cui anche il tipo di MergeOption attivo nel momento dell’invocazione. Quest’ultimo è un particolare importante il cui dettaglio non avevo trovato nella documentazione ufficiale. Nel secondo caso indicato sopra succede che la prima volta viene invocata la query compilata con MergeOption.NoTracking e questo valore viene associato alla query compilata per cui quando dopo vado a richiamare la stessa query compilata per modificare un valore nel momento in cui si esegue SaveChanges la query fallisce perche’ risulta che è attivo il NoTracking. Notare che non cambia nulla se vado ad aggiungere un MergeOption.AppendOnly nel metodo SetUserName. Non cambia nulla anche se richiamo la query con un diverso ObjectContext; insomma, il MergeOption è associato indissolubilmente con la query compilata.
La soluzione è quella di duplicare la compilazione della query o meglio fare una compilazione separata per ogni valore della MergeOption che si intende usare. Purtroppo si ripeterebbe piu’ volte il codice della query LINQ. La soluzione ottimale è quella di usare un CompiledQueryReplicator che è una classe che permette di specificare la query LINQ una volta sola e automaticamente di compilare piu’ istanze della stessa. Le istanze vengono inserite in un Dictionary la cui Key è il tipo di MergeOption con cui si vuole compilare la query. L’idea è stata già esplicitata in questo progetto su CodePlex : http://dcutilities.codeplex.com e li si puo’ andare a recuperare le due classi CompiledQueryReplicator e EfCompiledQueryReplicatorFactory. Consiglio anche di leggere il tutorial per la spiegazione di come usarle. Di seguito riporto per semplicità il codice delle due classi.

    /// <summary>
    /// The CompiledQueryReplicator is a class that allows you to specify a LINQ query once, then
    /// automatically compile different instances of it. This is useful when you need multiple instances
    /// of the same query compiled with different <see cref="System.Data.Objects.MergeOption"/>s, 
    /// for example. Each instance is identified with a particular key of type <typeparamref name="TKey"/>.
    /// </summary>
    /// <remarks>
    /// <para>
    /// If you're using the Entity Framework (LINQ to Entities), it is suggested that you use the 
    /// <see cref="EfCompiledQueryReplicatorFactory{TKey}"/> class to instantiate instances of this
    /// class, as it makes the syntax shorter and automatically provides you with the Entity Framework
    /// compiled query compiler.
    /// </para>
    /// <para>
    /// This class is thread-safe.
    /// </para>
    /// </remarks>
    /// <typeparam name="TQuery">The type of the query</typeparam>
    /// <typeparam name="TKey">The type to use as the key</typeparam>
    public class CompiledQueryReplicator<TQuery, TKey>
    {
        private readonly Expression<TQuery> _QueryExpressionTree;
        private readonly Func<Expression<TQuery>, TQuery> _Compiler;
        private readonly IDictionary<TKey, TQuery> _CompiledQueries;

        /// <summary>
        /// Creates a CompiledQueryReplicator using the specified query that will be compiled with
        /// the specified compiler.
        /// </summary>
        /// <param name="query">The query's expression tree</param>
        /// <param name="compiler">
        /// A delegate that can compile the query expression tree into an actual query
        /// </param>
        public CompiledQueryReplicator(Expression<TQuery> query, Func<Expression<TQuery>, TQuery> compiler)
        {
            _QueryExpressionTree = query;
            _Compiler = compiler;
            _CompiledQueries = new Dictionary<TKey, TQuery>();
        }

        /// <summary>
        /// Returns the compiled query instance associated with the specified key. If
        /// no query instance has been created for the specified key, one will be
        /// and it will be returned.
        /// </summary>
        /// <param name="key">The key</param>
        /// <returns>The compiled query.</returns>
        public TQuery this[TKey key]
        {
            get
            {
                TQuery query;
                lock (_CompiledQueries)
                {
                    if (_CompiledQueries.ContainsKey(key) == false)
                    {
                        query = _Compiler(_QueryExpressionTree);
                        _CompiledQueries.Add(key, query);
                    }
                    else
                        query = _CompiledQueries[key];
                }
                return query;
            }
        }
    }

    /// <summary>
    /// Factory class that makes it easier to create <see cref="CompiledQueryReplicator{TQuery,TKey}"/>
    /// instances that hold LINQ to Entities compiled queries.
    /// </summary>
    /// <typeparam name="TKey">T
    /// he type to use as the key for the CompiledQueryReplicators created
    /// </typeparam>
    public static class EfCompiledQueryReplicatorFactory<TKey>
    {
        /// <summary>
        /// Creates a <see cref="CompiledQueryReplicator{TQuery,TKey}"/> that compiles LINQ to Entities
        /// queries.
        /// </summary>
        /// <typeparam name="TArg0">
        /// The <see cref="ObjectContext"/> that encapsulates the model connection and metadata.
        /// </typeparam>
        /// <typeparam name="TResult">
        /// The type of the query results returned by executing the delegate returned by the Compile method.
        /// </typeparam>
        /// <param name="query">The LINQ to Entities query expression to compile</param>
        /// <returns>The <see cref="CompiledQueryReplicator{TQuery,TKey}"/> created</returns>
        public static CompiledQueryReplicator<Func<TArg0, TResult>, TKey> Create<TArg0, TResult>(Expression<Func<TArg0, TResult>> query)
            where TArg0 : ObjectContext
        {
            return new CompiledQueryReplicator<Func<TArg0, TResult>, TKey>(query, CompiledQuery.Compile);
        }

        /// <summary>
        /// Creates a <see cref="CompiledQueryReplicator{TQuery,TKey}"/> that compiles LINQ to Entities
        /// queries.
        /// </summary>
        /// <typeparam name="TArg0">
        /// The <see cref="ObjectContext"/> that encapsulates the model connection and metadata.
        /// </typeparam>
        /// <typeparam name="TArg1">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TResult">
        /// The type of the query results returned by executing the delegate returned by the Compile method.
        /// </typeparam>
        /// <param name="query">The LINQ to Entities query expression to compile</param>
        /// <returns>The <see cref="CompiledQueryReplicator{TQuery,TKey}"/> created</returns>
        public static CompiledQueryReplicator<Func<TArg0, TArg1, TResult>, TKey> Create<TArg0, TArg1, TResult>(Expression<Func<TArg0, TArg1, TResult>> query)
            where TArg0 : ObjectContext
        {
            return new CompiledQueryReplicator<Func<TArg0, TArg1, TResult>, TKey>(query, CompiledQuery.Compile);
        }

        /// <summary>
        /// Creates a <see cref="CompiledQueryReplicator{TQuery,TKey}"/> that compiles LINQ to Entities
        /// queries.
        /// </summary>
        /// <typeparam name="TArg0">
        /// The <see cref="ObjectContext"/> that encapsulates the model connection and metadata.
        /// </typeparam>
        /// <typeparam name="TArg1">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TArg2">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TResult">
        /// The type of the query results returned by executing the delegate returned by the Compile method.
        /// </typeparam>
        /// <param name="query">The LINQ to Entities query expression to compile</param>
        /// <returns>The <see cref="CompiledQueryReplicator{TQuery,TKey}"/> created</returns>
        public static CompiledQueryReplicator<Func<TArg0, TArg1, TArg2, TResult>, TKey> Create<TArg0, TArg1, TArg2, TResult>(Expression<Func<TArg0, TArg1, TArg2, TResult>> query)
            where TArg0 : ObjectContext
        {
            return new CompiledQueryReplicator<Func<TArg0, TArg1, TArg2, TResult>, TKey>(query, CompiledQuery.Compile);
        }

        /// <summary>
        /// Creates a <see cref="CompiledQueryReplicator{TQuery,TKey}"/> that compiles LINQ to Entities
        /// queries.
        /// </summary>
        /// <typeparam name="TArg0">
        /// The <see cref="ObjectContext"/> that encapsulates the model connection and metadata.
        /// </typeparam>
        /// <typeparam name="TArg1">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TArg2">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TArg3">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TResult">
        /// The type of the query results returned by executing the delegate returned by the Compile method.
        /// </typeparam>
        /// <param name="query">The LINQ to Entities query expression to compile</param>
        /// <returns>The <see cref="CompiledQueryReplicator{TQuery,TKey}"/> created</returns>
        public static CompiledQueryReplicator<Func<TArg0, TArg1, TArg2, TArg3, TResult>, TKey> Create<TArg0, TArg1, TArg2, TArg3, TResult>(Expression<Func<TArg0, TArg1, TArg2, TArg3, TResult>> query)
            where TArg0 : ObjectContext
        {
            return new CompiledQueryReplicator<Func<TArg0, TArg1, TArg2, TArg3, TResult>, TKey>(query, CompiledQuery.Compile);
        }
    }

Il codice di esempio riscritto con queste classi diventa :

public class QueryCompiled 
{
  public static readonly CompiledQueryReplicator<Func<MyEntities, string, Users>, MergeOption> 
             GetUserByEmail = EfCompiledQueryReplicatorFactory<MergeOption>.Create( 
                                     (MyEntities ctx, string email) => 
                                     (from a in ctx.Users 
                                     where a.Email == email 
                                     select a).FirstOrDefault());
}

public class A
{
  public string GetUserName(string email)
  {
    using (MyEntities context = new MyEntities()) 
    {
      context.Users.MergeOption = MergeOption.NoTracking; 
      var users= (QueryCompiled.GetUserByEmail[MergeOption.NoTracking](context, email));
      return users.Name;
    }
  }

  public void SetUserName(string email, string name)
  {
    using (MyEntities context = new MyEntities()) 
    {
      var users= (QueryCompiled.GetUserByEmail[MergeOption.AppendOnly](context, email)); 
      users.Name = name;
      context.SaveChanges();
    }
  }
}

Da notare che nell’invocazione della query si specifica il tipo di MergeOption che si intende usare. Se esiste una query compilata con quell’opzione nel dizionario viene ritornata altrimenti ne viene creata una.

Real Code Day 4.0 … un evento perfetto.

Leggo solo ora dal blog di AspItalia questo post : Online (quasi tutti) i video di Real Code Day 4. Era un evento organizzato ai primi di Dicembre a Firenze. Era uno di quegli eventi che avrei voluto seguire ma che la distanza rendeva impossibile. La disponibilità delle registrazioni video è una piacevole sorpresa; finalmente posso andare a vedere ed ascoltare cosa è stato presentato. Complimenti agli organizzatori per l’idea ! Leggo poi che l’evento era pure trasmesso online in streaming (Real Code Day 4.0 anche in streaming!). Ancora complimenti !
E dal post di ringraziamento (Real Code Day 4.0: grazie, demo e Azure) leggo che “ci sono stati dei picchi di connessioni molto ma molto interessanti”.
Streaming online e registrazioni video sono due elementi che reputo fondamentali per dire che questo è un evento perfetto. Dovrebbe essere di modello a tutti gli eventi. Purtroppo dopo l’abbandono di Microsoft alla moda dei webcast online si è preferito puntare alla moda delle community locali che hanno, secondo il mio parere, lo svantaggio di diluire sul territorio le conoscenze e renderle difficilmente fruibili. Almeno per me non è per nulla pratico andare personalmente agli eventi organizzati nelle varie città italiane. Diventa una sorta di giro d’Italia dello sviluppatore. Lo so che per molti è piu’ importante incontrare gli amici o la cena sociale con annessa gara a chi mangia la bistecca più grande. A me interessano puramente i contenuti. Purtroppo gli eventi organizzati non vengono mai registrati o se viene fatto le registrazioni sono mantenute segrete. Si vedono solo gli annunci con bei titoli sugli argomenti, spesso molto interessanti, si vedono poi le foto fatte delle facce soddisfatte del pubblico. Qualcuno rende disponibili le slide. Ma il risultato finale è che solo pochi fortunati sapranno cosa è stato proferito. Mi ricordano molto le società segrete carbonare o le logge massoniche o le sette religiose.

Questo Real Code Day invece è stato fatto con lo spirito della massima divulgazione. Ancora bravi ! La speranza è che anche gli altri eventi vengano fatti secondo questo “pattern”. Se l’esperienza dell’online ha dato picchi molto interessanti vuol dire che c’è un popolo che apprezzerebbe una maggiore diffusione. Scusate lo sfogo ma è una cosa che ho in testa da tanto tempo. Spero solo non sia una fissa tutta e solo mia.

[TFS] Togliere i log del database per guadagnare spazio

Ieri mentre facevo l’ultimo Checkin della giornata ottengo l’errore TF30042: The database is full. Momento di panico.
Oggi con calma trovo la soluzione. Il problema è causato dalla crescita abnorme dei file di log del database con conseguente rienpimento del disco. Il TFS server gira su una VM e quindi non ha molto spazio di disco. Avrei potuto aumentare la dimensione della VM ma cercando in giro ho trovato una soluzione che mi sembra piu’ adatta alla mia configurazione : TF30042: The database is full. Contact your Team Foundation Server administrator.
In sostanza viene consigliato di usare i database di TFS in Simple Recovery mode, ossia senza file di log. Nel mio caso cio’ non è grave poichè sul database di TFS ho aggiunto un job per fare il backup di tutti i database ogni notte.