Модульное тестирование сопоставлений NHibernate с SQLite и DateTimeOffset - PullRequest
14 голосов
/ 21 февраля 2010

Портирование через приложение для использования NHibernate из другого ORM.

Я начал использовать возможность запуска наших модульных тестов для базы данных SQLite в памяти. Это работает на первых нескольких партиях тестов, но я попал в ловушку. Наше приложение в реальном мире будет общаться с сервером SQL 2008, и поэтому некоторые модели в настоящее время имеют свойство DateTimeOffset. При отображении в / из SQL 2008 в не тестовых приложениях все это прекрасно работает.

Существует ли какой-либо механизм настройки базы данных или другого средства, чтобы при использовании сеанса из моего тестового устройства SQLite, что материал DateTimeOffset обрабатывался автоматически, как более независимый от платформы DateTime?

Ответы [ 3 ]

13 голосов
/ 21 февраля 2010

По совпадению, я только что сам решил эту проблему сегодня :) Я не проверил это решение полностью, и я новичок в 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());
    }
}
5 голосов
/ 19 января 2011

Еще одно предложение, которое позволяет отслеживать исходное смещение часового пояса :

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

Свободное соглашение NNibernate от 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>();
    }
}
3 голосов
/ 24 января 2011

Поскольку у меня недостаточно репутации, я не могу добавить это как комментарий к принятому ответу, но хотел добавить некоторую дополнительную информацию, которую я нашел при реализации решения в принятом ответе. Я тоже получал ошибку, что диалект не поддерживает DateTimeOffset при вызове экспорта схемы. После добавления поддержки журналирования в log4net я смог выяснить, что мои свойства имеют тип DateTimeOffset? не были обработаны соглашением. То есть соглашение не применялось к значениям nullable DateTimeOffset.

Чтобы решить эту проблему, я создал класс, который извлекается из NormalizedDateTimeUserType и переопределяет свойство ReturnedType (нужно было отметить оригинал как виртуальный). Затем я создал второе UserTypeConvention для моего класса Derrived и, наконец, добавил второе соглашение в мою конфигурацию.

...