2010-02-21 55 views
14

將應用程序移植到不同的ORM中使用NHibernate。單元測試NHibernate w/SQLite和DateTimeOffset映射

我已經開始對內存SQLite數據庫運行我們的單元測試的能力。這適用於前幾批測試,但我只是碰到了一個障礙。我們的應用程序將在現實世界中與SQL 2008服務器交談,因此,目前有幾個模型具有DateTimeOffset屬性。在非測試應用程序中映射到/從SQL 2008映射時,這一切都可以正常工作。

是否有一些機制在配置數據庫或其他設施,以便當我從我的SQLite測試夾具使用DateTimeOffset的東西是「自動神奇地」處理爲更平臺不可知的DateTime?

回答

13

巧合的是,我今天自己就碰到了這個問題:)我還沒有徹底地測試過這個解決方案,而且我是NHibernate的新手,但它似乎可以在我嘗試過的微不足道的情況下工作。

首先,您需要創建一個將從DateTimeOffset轉換爲DateTime的IUserType實現。還有如何創建一個用戶類型on the Ayende blog但我們的目的相關方法的實現是一個完整的例子:

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; 
     } 
    } 
} 

databaseTimeZone領域擁有TimeZone它描述了用於存儲值在數據庫中的時區。在存儲之前,所有DateTimeOffset值都轉換爲此時區。在我當前的實現中,它被硬編碼到本地時區,但是您可以始終定義一個ITimeZoneProvider接口並將其注入到構造函數中。

要在不改變我所有的類映射使用此用戶類型,我創建了流利的NH公約:

public class NormalizedDateTimeUserTypeConvention : UserTypeConvention<NormalizedDateTimeUserType> 
{ 
} 

,我在我的映射應用這個約定,如本例(該new NormalizedDateTimeUserTypeConvention()是最重要的部分):

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

就像我說的,這是沒有徹底測試,所以要小心!但現在,我需要做的就是修改一行代碼(流暢映射規範),並且可以在數據庫中的DateTime和DateTimeOffset之間切換。


編輯

按照要求,功能NHibernate配置:

要建立SQL Server的一個會話工廠:

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

SQLite的:

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

實施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

非常好!現在給出這個鏡頭,可能會稍微改變一下,所以我可以根據上下文注入適當的會話工廠,但是這有希望會讓我走得更遠。在保證我的映射將自己推入SQLite模式方面,有什麼特別需要注意? – bakasan 2010-02-22 03:42:05

+0

我能想到的唯一的事情是(正如我確信你知道的那樣)在SQLite中沒有強制實施FK約束,所以你可能應該有一套在生產平臺上運行的測試。 我目前每個存儲庫都維護兩套測試 - 一種可以針對SQLite運行NHib的東西(級聯,映射等),另一種針對SQL Server運行以測試數據庫的東西(約束等)。 – 2010-02-22 09:58:10

+1

當我嘗試將我的模式應用於內存數據庫時,我似乎遇到了運行時問題 - 您會介意在您建立數據庫選項的位置共享您的Fluent NH配置嗎?錯誤消息基本上是我的方言不支持DateTimeOffset(duh;))..我的數據庫被定義爲數據庫(SQLiteConfiguration.Standard.InMemory()) – bakasan 2010-02-24 07:34:58

5

這允許另一項建議跟蹤到原始時區偏移

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; 
    } 
} 
從的DateTimeOffset ICompositeUserType

流利NNibernate公約將是:

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

接口稍有變化--bool []可設置添加: void NullSafeSet(IDbCommand cmd,object value,int index,bool [] settable,ISessionImplementor session); – user1656671 2014-04-14 13:52:57

3

由於我代表短暫,我無法將此添加爲對接受的答案發表評論,但希望在接受的答案中實施解決方案時添加一些額外的信息。調用模式導出時,我也遇到了方言不支持DateTimeOffset的錯誤。在添加log4net日誌記錄支持後,我能夠弄清楚我的屬性是DateTimeOffset類型的嗎?沒有被公約處理。也就是說,該約定不適用於可空的 DateTimeOffset屬性。

爲了解決這個問題,我創建了一個從NormalizedDateTimeUserType開始衍生並重寫ReturnedType屬性的類(必須將原始標記爲虛擬)。然後,我爲我的耙類創建了第二個UserTypeConvention,並最終將第二個約定添加到了我的配置中。

+1

我也必須這樣做 - 應該在我原來的答案中提到。 – 2011-03-08 16:44:56