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