单元测试NHibernate w/SQLite和DateTimeOffset映射

单元测试NHibernate w/SQLite和DateTimeOffset映射,nhibernate,sqlite,datetimeoffset,Nhibernate,Sqlite,Datetimeoffset,在应用程序上进行移植以使用来自不同ORM的NHibernate 我已经开始对内存中的SQLite数据库运行单元测试。这在前几批测试中有效,但我遇到了一个障碍。在现实世界中,我们的应用程序将与SQL 2008服务器进行通信,因此,目前有几种型号具有DateTimeOffset属性。在非测试应用程序中映射到SQL 2008或从SQL 2008映射时,这一切都可以正常工作 在配置数据库或其他工具时是否存在某种机制,以便当我使用SQLite测试夹具中的会话时,DateTimeOffset内容“自动神奇地

在应用程序上进行移植以使用来自不同ORM的NHibernate

我已经开始对内存中的SQLite数据库运行单元测试。这在前几批测试中有效,但我遇到了一个障碍。在现实世界中,我们的应用程序将与SQL 2008服务器进行通信,因此,目前有几种型号具有DateTimeOffset属性。在非测试应用程序中映射到SQL 2008或从SQL 2008映射时,这一切都可以正常工作


在配置数据库或其他工具时是否存在某种机制,以便当我使用SQLite测试夹具中的会话时,DateTimeOffset内容“自动神奇地”处理为更不依赖于平台的DateTime?

巧合的是,我今天自己遇到了这个问题:)我还没有彻底测试这个解决方案,我是NHibernate的新手,但它似乎在我尝试过的琐碎案例中起作用

首先,您需要创建一个IUserType实现,该实现将从DateTimeOffset转换为DateTime。有一个关于如何创建用户类型的完整示例,但我们的相关方法实现如下:

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接口,并将其注入构造函数中

为了在不修改所有类映射的情况下使用此用户类型,我在Fluent NH中创建了一个约定:

public class NormalizedDateTimeUserTypeConvention : UserTypeConvention<NormalizedDateTimeUserType>
{
}
就像我说的,这不是彻底的测试,所以要小心!但是现在,我所需要做的就是修改一行代码(fluent映射规范),我可以在数据库中的DateTime和DateTimeOffset之间切换


编辑

根据要求,Fluent NHibernate配置:

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());
    }
}
要为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());
    }
}

另一个允许跟踪原始时区偏移的方案是:

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中的默认日期约定为:

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>();
    }
}
公共类DateTimeOffsetTypeConvention:IPropertyConvention,IPropertyConvention Acceptance
{
公共无效接受(IAcceptanceCriteria标准)
{
Expect(x=>x.Type==typeof(DateTimeOffset));
}
公共无效应用(IPropertyInstance实例)
{
CustomType();
}
}

由于我缺少代表,我无法将此作为评论添加到接受的答案中,但我想添加一些在接受的答案中实施解决方案时发现的附加信息。调用模式导出时,我也遇到了一个错误,即方言不支持DateTimeOffset。在添加log4net日志记录支持后,我能够确定我的属性类型为DateTimeOffset?没有被公约处理。也就是说,该约定未应用于可为空的DateTimeOffset属性


为了解决这个问题,我创建了一个类,它从NormalizedDateTimeUserType派生并重写ReturnedType属性(必须将原始属性标记为virtual)。然后,我为我的驱动类创建了第二个UserTypeConvention,最后将第二个约定添加到我的配置中

非常好!现在给它一个镜头,可能会改变它一点,这样我就可以根据上下文注入适当的会话工厂,但这很有希望将推动我向前迈进。在保证我的映射将自己推入SQLite模式方面,我需要注意什么特别的事情吗?我唯一能想到的是(正如我确信您知道的)SQLite中没有强制执行FK约束,因此您可能也应该有一组在生产平台上运行的测试。目前,我为每个存储库维护两组测试—一组测试NHib内容(级联、映射等),可以针对SQLite运行,另一组测试SQL Server,以测试DB内容(约束等).当我尝试将模式应用于内存中的数据库时,我似乎遇到了运行时问题--您介意在构建数据库选项的地方共享Fluent NH配置吗?错误消息基本上是我的方言不支持DateTimeOffset(duh;)。。我的数据库被定义为数据库(SQLiteConfiguration.Standard.InMemory()),谢谢——从语义上讲,我们做的事情有点不同,但基本上是一样的。。但是每次我转到SchemaExport.Create()时,我都会发现方言不支持DateTimeOffset错误。某处我搞砸了…哎呀,猜猜看,还是继续挑这个吧。谢谢你的帮助!查看调试输出(我使用log4net TraceAppender),并在映射包含DateTimeOffset的类时检查输出。下面是我的输出的一个片段:NHibernate.Cfg.XmlHbmBinding.Binder:3913调试映射属性:CreatedDate->CreatedDate,类型:NormalizedDateTimeUserType如果您没有看到类似的内容,那么可能NH出于某种原因没有注册IUserType?我也必须这样做-应该在我的原始答案中提到。接口略有更改-bool[]settable添加了:void NullSafeSet(IDbCommand cmd,object value,int index,bool[]可设置,iSession(实施者会话);