2010-02-21 10 views
14

Porting su un'applicazione per utilizzare NHibernate da un ORM differente.Test dell'unità NHibernate con mapping SQLite e DateTimeOffset

Ho iniziato a mettere in atto la possibilità di eseguire i nostri test unitari su un database SQLite in memoria. Funziona sui primi gruppi di test, ma ho appena colpito un ostacolo. La nostra app nel mondo reale starebbe parlando con un server SQL 2008 e, come tale, diversi modelli attualmente hanno una proprietà DateTimeOffset. Quando si esegue il mapping a/da SQL 2008 in applicazioni non di test, tutto funziona correttamente.

C'è qualche meccanismo nella configurazione del database o in qualche altra funzione in modo tale che quando uso una sessione dal mio dispositivo di prova SQLite che il materiale DateTimeOffset sia gestito "auto magicamente" come il più indipendente DateTime della piattaforma?

risposta

13

Per coincidenza, ho appena colpito questo problema io stesso oggi :) Non ho testato questa soluzione a fondo, e sono nuovo di NHibernate, ma sembra funzionare nel caso banale che ho provato.

Per prima cosa è necessario creare un'implementazione IUserType che convertirà da DateTimeOffset a DateTime. C'è un esempio completo di come creare un tipo di utente on the Ayende blog ma le implementazioni di metodo rilevanti per i nostri scopi sono:

public class NormalizedDateTimeUserType : IUserType 
{ 
    private readonly TimeZoneInfo databaseTimeZone = TimeZoneInfo.Local; 

    // Other standard interface implementations omitted ... 

    public Type ReturnedType 
    { 
     get { return typeof(DateTimeOffset); } 
    } 

    public SqlType[] SqlTypes 
    { 
     get { return new[] { new SqlType(DbType.DateTime) }; } 
    } 

    public object NullSafeGet(IDataReader dr, string[] names, object owner) 
    { 
     object r = dr[names[0]]; 
     if (r == DBNull.Value) 
     { 
      return null; 
     } 

     DateTime storedTime = (DateTime)r; 
     return new DateTimeOffset(storedTime, this.databaseTimeZone.BaseUtcOffset); 
    } 

    public void NullSafeSet(IDbCommand cmd, object value, int index) 
    { 
     if (value == null) 
     { 
      NHibernateUtil.DateTime.NullSafeSet(cmd, null, index); 
     } 
     else 
     { 
      DateTimeOffset dateTimeOffset = (DateTimeOffset)value; 
      DateTime paramVal = dateTimeOffset.ToOffset(this.databaseTimeZone.BaseUtcOffset).DateTime; 

      IDataParameter parameter = (IDataParameter)cmd.Parameters[index]; 
      parameter.Value = paramVal; 
     } 
    } 
} 

Il campo databaseTimeZone detiene una TimeZone che descrive il fuso orario che viene utilizzato per memorizzare i valori nel database. Tutti i valori DateTimeOffset vengono convertiti in questo fuso orario prima della memorizzazione. Nella mia attuale implementazione è hard-coded al fuso orario locale, ma è sempre possibile definire un'interfaccia ITimeZoneProvider e inserirla in un costruttore.

Per utilizzare questo tipo di utente senza modificare tutte le mie mappe di classe, ho creato una convenzione in Fluent NH:

public class NormalizedDateTimeUserTypeConvention : UserTypeConvention<NormalizedDateTimeUserType> 
{ 
} 

e ho applicato questa convenzione nei miei mappature, come in questo esempio (il new NormalizedDateTimeUserTypeConvention() è l'importante parte):

mappingConfiguration.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly()) 
       .Conventions.Add(
       PrimaryKey.Name.Is(x => x.EntityType.Name + "Id"), 
       new NormalizedDateTimeUserTypeConvention(), 
       ForeignKey.EndsWith("Id")); 

Come ho detto, questo non è stato testato a fondo, quindi fate attenzione! Ma ora tutto ciò che devo fare è modificare una riga di codice (la specifica fluente dei mapping) e posso passare da DateTime a DateTimeOffset nel database.


Modifica

Come richiesto, la configurazione Fluent NHibernate:

per costruire una fabbrica di sessione per SQL Server:

private static ISessionFactory CreateSessionFactory(string connectionString) 
{ 
    return Fluently.Configure() 
      .Database(MsSqlConfiguration.MsSql2008.ConnectionString(connectionString)) 
      .Mappings(m => MappingHelper.SetupMappingConfiguration(m, false)) 
      .BuildSessionFactory(); 
} 

per SQLite:

return Fluently.Configure() 
      .Database(SQLiteConfiguration.Standard.InMemory) 
      .Mappings(m => MappingHelper.SetupMappingConfiguration(m, true)) 
      .ExposeConfiguration(cfg => configuration = cfg) 
      .BuildSessionFactory(); 

Attuazione della SetupMappingConfiguration:

public static void SetupMappingConfiguration(MappingConfiguration mappingConfiguration, bool useNormalizedDates) 
{ 
    mappingConfiguration.FluentMappings 
     .AddFromAssembly(Assembly.GetExecutingAssembly()) 
     .Conventions.Add(
      PrimaryKey.Name.Is(x => x.EntityType.Name + "Id"), 
      ForeignKey.EndsWith("Id")); 

    if (useNormalizedDates) 
    { 
     mappingConfiguration.FluentMappings.Conventions.Add(new NormalizedDateTimeUserTypeConvention()); 
    } 
} 
+0

Molto bello! Dare una possibilità in questo momento, probabilmente cambierà un po 'in modo da poter iniettare la sessione appropriata in base al contesto, ma spero che questo mi spingerà lontano. C'è qualcosa di speciale di cui devo essere consapevole in termini di garanzia che le mie mappature si spingano nello schema SQLite? – bakasan

+0

L'unica cosa che posso pensare è che (come sono sicuro tu sappia) non ci sono vincoli FK applicati in SQLite, quindi probabilmente dovresti avere una serie di test che girano anche sulla piattaforma di produzione. Attualmente mantengo due serie di test per repository, uno che esercita la roba di NHib (cascate, mappature ecc.) Che può essere eseguito su SQLite e uno che viene eseguito su SQL Server per testare la roba DB (vincoli, ecc.). –

+1

Sembra che si stiano verificando problemi di runtime quando tento di applicare il mio schema alla memoria in db-- ti dispiacerebbe condividere la tua configurazione di Fluent NH dove costruisci le tue opzioni db? Il messaggio di errore è fondamentalmente che il mio dialetto non supporta DateTimeOffset (duh;)) .. il mio db è definito come Database (SQLiteConfiguration.Standard.InMemory()) – bakasan

5

Un'altra proposta che permettono di tenere traccia di fuso orario originale Offset:

public class DateTimeOffsetUserType : ICompositeUserType 
{ 
    public string[] PropertyNames 
    { 
     get { return new[] { "LocalTicks", "Offset" }; } 
    } 

    public IType[] PropertyTypes 
    { 
     get { return new[] { NHibernateUtil.Ticks, NHibernateUtil.TimeSpan }; } 
    } 

    public object GetPropertyValue(object component, int property) 
    { 
     var dto = (DateTimeOffset)component; 

     switch (property) 
     { 
      case 0: 
       return dto.UtcTicks; 
      case 1: 
       return dto.Offset; 
      default: 
       throw new NotImplementedException(); 
     } 
    } 

    public void SetPropertyValue(object component, int property, object value) 
    { 
     throw new NotImplementedException(); 
    } 

    public Type ReturnedClass 
    { 
     get { return typeof(DateTimeOffset); } 
    } 

    public new bool Equals(object x, object y) 
    { 
     if (ReferenceEquals(x, null) && ReferenceEquals(y, null)) 
      return true; 

     if (ReferenceEquals(x, null) || ReferenceEquals(y, null)) 
      return false; 

     return x.Equals(y); 
    } 

    public int GetHashCode(object x) 
    { 
     return x.GetHashCode(); 
    } 

    public object NullSafeGet(IDataReader dr, string[] names, ISessionImplementor session, object owner) 
    { 
     if (dr.IsDBNull(dr.GetOrdinal(names[0]))) 
     { 
      return null; 
     } 

     var dateTime = (DateTime)NHibernateUtil.Ticks.NullSafeGet(dr, names[0], session, owner); 
     var offset = (TimeSpan)NHibernateUtil.TimeSpan.NullSafeGet(dr, names[1], session, owner); 

     return new DateTimeOffset(dateTime, offset); 
    } 

    public void NullSafeSet(IDbCommand cmd, object value, int index, ISessionImplementor session) 
    { 
     object utcTicks = null; 
     object offset = null; 

     if (value != null) 
     { 
      utcTicks = ((DateTimeOffset)value).DateTime; 
      offset = ((DateTimeOffset)value).Offset; 
     } 

     NHibernateUtil.Ticks.NullSafeSet(cmd, utcTicks, index++, session); 
     NHibernateUtil.TimeSpan.NullSafeSet(cmd, offset, index, session); 
    } 

    public object DeepCopy(object value) 
    { 
     return value; 
    } 

    public bool IsMutable 
    { 
     get { return false; } 
    } 

    public object Disassemble(object value, ISessionImplementor session) 
    { 
     return value; 
    } 

    public object Assemble(object cached, ISessionImplementor session, object owner) 
    { 
     return cached; 
    } 

    public object Replace(object original, object target, ISessionImplementor session, object owner) 
    { 
     return original; 
    } 
} 

perfetto NNibernate convenzione da DateTimeOffset ICompositeUserType sarebbe:

public class DateTimeOffsetTypeConvention : IPropertyConvention, IPropertyConventionAcceptance 
{ 
    public void Accept(IAcceptanceCriteria<IPropertyInspector> criteria) 
    { 
     criteria.Expect(x => x.Type == typeof(DateTimeOffset)); 
    } 

    public void Apply(IPropertyInstance instance) 
    { 
     instance.CustomType<DateTimeOffsetUserType>(); 
    } 
} 
+0

L'interfaccia è leggermente cambiata - bool [] impostabile impostabile: void NullSafeSet (IDbCommand cmd, valore oggetto, indice int, bool [] impostabile, sessione ISessionImplementor); – user1656671

3

Siccome sono a corto di rappresentante non posso aggiungere questo come un commentare la risposta accettata, ma ha voluto aggiungere alcune informazioni aggiuntive che ho trovato mentre implementavo la soluzione nella risposta accettata. Anch'io ricevevo l'errore che il dialetto non supporta DateTimeOffset quando si chiama l'esportazione dello schema. Dopo aver aggiunto il supporto per la registrazione di log4net, sono riuscito a capire che le mie proprietà erano di tipo DateTimeOffset? non erano gestiti dalla convenzione. Cioè, la convenzione non veniva applicata a nullable Proprietà DateTimeOffset.

Per risolvere questo problema, ho creato una classe che designa da NormalizedDateTimeUserType e sovrascrive la proprietà ReturnedType (doveva contrassegnare l'originale come virtuale). Quindi ho creato un secondo UserTypeConvention per la mia classe derrived e infine aggiunto la seconda convenzione alla mia configurazione.

+1

Ho dovuto farlo anche io - avrei dovuto menzionare nella mia risposta originale. –

Problemi correlati