移植应用程序以从不同的 ORM 使用 NHibernate。
我已经开始具备针对内存 SQLite 数据库运行单元测试的能力。这适用于前几批测试,但我遇到了障碍。在现实世界中,我们的应用程序将与 SQL 2008 服务器通信,因此,当前有多个模型具有 DateTimeOffset 属性。在非测试应用程序中映射到 SQL 2008 或从 SQL 2008 映射时,这一切都工作正常。
在配置数据库或其他设施时是否有某种机制,以便当我使用 SQLite 测试装置中的会话时,DateTimeOffset 内容会“自动神奇地”处理为与平台无关的 DateTime?
巧合的是,我今天自己也遇到了这个问题:)我还没有彻底测试这个解决方案,而且我是NHibernate的新手,但它似乎在我尝试过的简单情况下工作。
首先,您需要创建一个 IUserType 实现,它将从 DateTimeOffset 转换为 DateTime。 Ayende 博客上有一个关于如何创建用户类型的完整示例,但与我们的目的相关的方法实现是:
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>
{
}
我在我的映射中应用了这个约定,如本例所示(
new NormalizedDateTimeUserTypeConvention()
是重要的部分):
mappingConfiguration.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())
.Conventions.Add(
PrimaryKey.Name.Is(x => x.EntityType.Name + "Id"),
new NormalizedDateTimeUserTypeConvention(),
ForeignKey.EndsWith("Id"));
就像我说的,这还没有经过彻底的测试,所以要小心!但现在,我需要做的就是更改一行代码(流畅的映射规范),我就可以在数据库中的 DateTime 和 DateTimeOffset 之间切换。
编辑
根据要求,Fluent 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());
}
}
跟踪原始时区偏移量:
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>();
}
}
nullable DateTimeOffset 属性。
为了解决这个问题,我创建了一个派生自 NormalizedDateTimeUserType 的类并覆盖 ReturnedType 属性(必须将原始属性标记为虚拟)。然后,我为派生类创建了第二个 UserTypeConvention,最后将第二个约定添加到我的配置中。