使用 NHibernate 保存和加载 Utc DateTime

Save and load Utc DateTime with NHibernate

我在将 DateTime 保存到 SQL Lite 数据库时遇到问题。 (也许还有 MS SQL)

我想使用 NHibernate 将 UTC 时间的 DateTime 保存到数据库并从数据库加载它。我们在 hole 应用程序中使用 utc 时间工作,当我们在 ui 上显示时间时,我们将其更改为本地时间。

我阅读了很多关于 DateTime 和 NHibernate 的文章:

但没有任何效果。 一些例子:

PreUpdate: 在保存实体之前。

Saved:是用nhibernate保存后保存的对象(repo.save(o);).

已加载:当我通过 id 从存储库加载实体时。

// The next 3 examples are with:
o.Created = DateTime.UtcNow;

映射类型:CustomType<TimestampType>()

协调时间:16:44...本地时间:18:44

这里的问题是,当我通过 id 重新加载对象时,新对象的时间是 18... (+2h) 而不是 16.... 并且 DateTime 类型是未指定的。

映射类型:CustomType<DateTimeType>()

协调时间:16:49...本地时间:18:49

使用此解决方案,我放宽了毫秒数,并且 DateTime 种类也是未指定的。

映射类型:CustomType<UtcDateTimeType>()

协调时间:17:01...本地时间:19:01

通过这个解决方案,我放宽了毫秒数,DateTime 类型是 utc 但它是错误的时间,应该是 17:01...

所以另一个想法是仅在应用程序中使用 DateTime.Now 并将 utc 时间保存在数据库中。一些例子:

// The next 3 examples are with:
o.Created = DateTime.Now;

映射类型:CustomType<TimestampType>()

协调时间:17:21...本地时间:19:21

通过这个解决方案,我得到了毫秒数,DateTime 类型是未指定的,加载时间不是 utc。

映射类型:CustomType<DateTimeType>()

协调时间:17:19...本地时间:19:19

使用此解决方案,我放宽了毫秒数,DateTime 类型也是未指定的,加载时间不是 utc。

映射类型:CustomType<UtcDateTimeType>()

协调时间:17:14...本地时间:19:14

通过这个解决方案,我放宽了毫秒数,DateTime 类型是 utc 但它是错误的时间,应该是 17:14...

所以我有一些问题:

  1. 为什么 NHibernate 加载本地时间但使用类型为 utc(UtcDateTimeType 和 o.Created=DateTime.UtcNow)
  2. 在孔应用程序中和 UI localtime 中使用 utc 还是在所有地方都使用 localtime 并将时间 utc 保存在数据库中更好。

我还创建了自己的映射:

namespace Persistence.Common.NHibernate
{
    using System;
    using System.Data;

    using global::NHibernate.Engine;
    using global::NHibernate.Type;

    /// <summary>
    /// This is almost the exact same type as the DateTime except it can be used
    /// in the version column, stores it to the accuracy the database supports, 
    /// and will default to the value of DateTime.Now if the value is null.
    /// </summary>
    /// <remarks>
    /// <p>
    /// The value stored in the database depends on what your data provider is capable
    /// of storing.  So there is a possibility that the DateTime you save will not be
    /// the same DateTime you get back when you check DateTime.Equals(DateTime) because
    /// they will have their milliseconds off.
    /// </p>  
    /// <p>
    /// For example - SQL Server 2000 is only accurate to 3.33 milliseconds.  So if 
    /// NHibernate writes a value of <c>01/01/98 23:59:59.995</c> to the Prepared Command, MsSql
    /// will store it as <c>1998-01-01 23:59:59.997</c>.
    /// </p>
    /// <p>
    /// Please review the documentation of your Database server.
    /// </p>
    /// </remarks>
    [Serializable]
    public class CustomUtcTimestampType : TimestampType
    {
        public CustomUtcTimestampType()
        {
        }

        public override object Get(IDataReader rs, int index)
        {
            return Convert.ToDateTime(rs[index]).ToLocalTime();
        }

        /// <summary>
        /// Sets the value of this Type in the IDbCommand.
        /// </summary>
        /// <param name="st">The IDbCommand to add the Type's value to.</param>
        /// <param name="value">The value of the Type.</param>
        /// <param name="index">The index of the IDataParameter in the IDbCommand.</param>
        /// <remarks>
        /// No null values will be written to the IDbCommand for this Type. 
        /// </remarks>
        public override void Set(IDbCommand st, object value, int index)
        {
            DateTime dateTime = (DateTime)((value is DateTime) ? value : DateTime.UtcNow);
            dateTime = DateTime.SpecifyKind(dateTime.ToUniversalTime(), DateTimeKind.Unspecified);
            ((IDataParameter)st.Parameters[index]).Value = dateTime;
        }

        public override string Name
        {
            get { return "CustomUtcTimestamp"; }
        }

        public override object FromStringValue(string xml)
        {
            return DateTime.Parse(xml);
        }

        #region IVersionType Members

        public override object Seed(ISessionImplementor session)
        {
            if (session == null)
            {
                return DateTime.UtcNow;
            }
            return Round(DateTime.UtcNow, session.Factory.Dialect.TimestampResolutionInTicks);
        }

        #endregion

        public object StringToObject(string xml)
        {
            return DateTime.Parse(xml);
        }

        public override string ObjectToSQLString(object value, global::NHibernate.Dialect.Dialect dialect)
        {
            return '\'' + value.ToString() + '\'';
        }
    }
}
  1. 将日期时间加载为 UTC 的行为,我相信使用类型 UtcDateTimeType 时的预期行为是假设时间集为 UTC,并且在获取时也将其视为 UTC,

如果您进行以下快速测试,

    public class UtcTime
    {
        public virtual long Id { get; set; }
        public virtual DateTime DateSaved { get; set; }
    }

    public class UtcTimeClassMap : ClassMap<UtcTime>
    {
        public UtcTimeClassMap()
        {
            Id(t => t.Id).GeneratedBy.Native();
            Map(t => t.DateSaved ).CustomType<UtcDateTimeType>();
        }
    }

    [Test]
    public void SimpleTest()
    {
        long id = 0;
        ISession session = _sessionFactory.OpenSession();
        using (ITransaction tran = session.BeginTransaction())
        {
            UtcTime utc = new UtcTime();
            utc.DateSaved = DateTime.Now;
            session.Save(utc);
            tran.Commit();

            Console.WriteLine(utc.DateSaved.Ticks + "_" + utc.DateSaved.Kind + "_" + utc.Date.ToString());

            id = utc.Id;
        }
        session.Flush();
        session.Clear();

        session = _sessionFactory.OpenSession();
        var retrieved = session.Get<UtcTime>(id);
        Console.WriteLine(retrieved.DateSaved.Ticks + "_" + retrieved.Date.Kind + "_" + retrieved.DateSaved.ToString());
    }

输出

INSERT INTO [UtcTime] (DateSaved) VALUES (?); select SCOPE_IDENTITY()
635634005813892469_Local_31/03/2015 12:09:41 PM
SELECT utctime0_.Id as Id3_0_, utctime0_.DateSaved as Date3_0_ FROM [UtcTime] utctime0_ WHERE utctime0_.Id=?
635634005810000000_Utc_31/03/2015 12:09:41 PM

即使我将 12:09:41 坚持为本地,当我取回它时它是 UTC,同时它假设一切都在 UTC 中发生。此测试是使用 SQLServer 数据库完成的。

如果我用 SQLite 重复相同的测试,输出是,

INSERT INTO "UtcTime" (DateSaved) VALUES (?); select last_insert_rowid()
635634005197863939_Local_31/03/2015 12:08:39 PM
SELECT utctime0_.Id as Id3_0_, utctime0_.DateSaved as Date3_0_ FROM "UtcTime" utctime0_ WHERE utctime0_.Id=?
635634401190000000_Utc_31/03/2015 11:08:39 PM

我可以在这里看到一个小时无法解释的差异(因为当地时间和进行测试的 UTC 之间的差异是 11 小时,这不能解释它。 我能想到的唯一解释是 SQLLite 方言中可能存在某种错误。

  1. 我认为保存 UTC 是更好的选择,尤其是当用户处于时区范围内时。但是,如果用户在同一时区,则没有转换点,因为这使得诊断错误特别是查询数据库变得困难,因为您必须始终转换值。

作为精确解决此问题的方法,您可以添加这样的自定义类型,

public class UTCTimeStampType : TimestampType
    {
        public override object Get(IDataReader rs, int index)
        {
            return ConvertToUtc(base.Get(rs, index));
        }

        public override object Get(IDataReader rs, string name)
        {
            return ConvertToUtc(base.Get(rs, name));
        }

        public override object FromStringValue(string xml)
        {
            return ConvertToUtc(base.FromStringValue(xml));
        }

        private DateTime ConvertToUtc(object value)
        {
            var dateTime = (DateTime) value;
            return new DateTime(dateTime.Ticks).ToUniversalTime();
        }
    }

感谢您的回答。使用您的自定义类型版本,我在加载日期时间时通常有错误的时间。问题是这段代码base.Get(rs, index)。当我执行此方法时,它将数据库中的 utc 时间转换为错误的 DateTime。我现在的解决方案是,以 utc 格式保存时间,但在我将 DateTimeKind 更改为 Unspecified 并将值存储在数据库 (SQLite) 之前。结果是最后没有 Z 的时间。当我加载时间时,我在加载 DateTime 后更改了 DateTimeKind,一切正常。

以下代码解决了我的问题:

using System;
using System.Data;

using global::NHibernate.Engine;
using global::NHibernate.Type;

using Foundation.Core;

/// <summary>
/// This type save the <see cref="DateTime"/> to the database. You need to save the <see cref="DateTime"/> in UTC (<see cref="DateTimeKind.Utc"/>).
/// When you load the <see cref="DateTime"/>, then time is in UTC.
/// </summary>
/// <seealso cref=""/>
public class UtcTimestampType : TimestampType
{
    public override string Name
    {
        get { return "UtcTimestamp"; }
    }

    /// <summary>
    /// Sets the value of this Type in the IDbCommand.
    /// </summary>
    /// <param name="st">The IDbCommand to add the Type's value to.</param>
    /// <param name="value">The value of the Type.</param>
    /// <param name="index">The index of the IDataParameter in the IDbCommand.</param>
    /// <remarks>
    /// No null values will be written to the IDbCommand for this Type.
    /// The <see cref="DateTime.Kind"/> must be <see cref="DateTimeKind.Utc"/>.
    /// </remarks>
    public override void Set(IDbCommand st, object value, int index)
    {
        DateTime dateTime = (DateTime)((value is DateTime) ? value : DateTime.UtcNow);
        Check.IsValid(() => dateTime, dateTime, time => time.Kind == DateTimeKind.Utc, "You need to save the date time in the utc format.");
        // Change the kind to unspecified, because when we load the datetime we have wrong values with kind utc.
        ((IDataParameter)st.Parameters[index]).Value = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);
    }

    public override object Get(IDataReader rs, int index)
    {
        return ChangeDateTimeKindToUtc(base.Get(rs, index));
    }

    public override object Get(IDataReader rs, string name)
    {
        return ChangeDateTimeKindToUtc(base.Get(rs, name));
    }

    public override object FromStringValue(string xml)
    {
        return ChangeDateTimeKindToUtc(base.FromStringValue(xml));
    }

    public override object Seed(ISessionImplementor session)
    {
        if (session == null)
        {
            return DateTime.UtcNow;
        }

        return Round(DateTime.UtcNow, session.Factory.Dialect.TimestampResolutionInTicks);
    }

    private DateTime ChangeDateTimeKindToUtc(object value)
    {
        DateTime dateTime = (DateTime)value;
        return new DateTime(dateTime.Ticks, DateTimeKind.Utc);
    }
}

您如何看待这两种方法?我需要这些吗?我应该使用 Round(DateTime.UtcNow... 吗?什么时候需要 FromStringValue

public override object FromStringValue(string xml)
    {
        return ChangeDateTimeKindToUtc(base.FromStringValue(xml));
    }

    public override object Seed(ISessionImplementor session)
    {
        if (session == null)
        {
            return DateTime.UtcNow;
        }

        return Round(DateTime.UtcNow, session.Factory.Dialect.TimestampResolutionInTicks);
    }

我遇到了同样的问题,在我的例子中,我试图将所有内容都作为 Utc 往返,字段映射为 UtcDateTimeType,但返回的时间标记为 UTC,但转换为本地时间。答案既简单又烦人。你必须 tell the SQLite engine not to convert DateTimes for you by adding a parameter to the connection string:

   config.DataBaseIntegration(c =>
        {
            c.Dialect<SQLiteDialect>();
            c.Driver<SQLite20Driver>();
            c.ConnectionString = "DataSource=MyAppDB.sqlite;DateTimeKind=Utc";
            //                                              ^^^^^^^^^^^^^^^^
            ...

这样做之后,我的应用程序表现得更加明智。