使用NHibernate保存并加载Utc DateTime

时间:2015-03-30 17:41:09

标签: c# datetime nhibernate utc

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

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

我读了很多关于DateTime和NHibernate的信息:

但没有任何作用。 一些例子:

PreUpdate :在保存实体之前。

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

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

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

Mappingtype:CustomType<TimestampType>()

UtcTime:16:44 ......当地时间:18:44

  • PreUpdate:2015-03-30T16:44:35.7636679Z Tick:635633306757636679种类:Utc
  • 已保存:2015-03-30T16:44:35.7636679Z Tick:635633306757636679种类:Utc
  • 已加载:2015-03-30T18:44:35.7636679 Tick:635633378757636679种类:未指定

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

Mappingtype:CustomType<DateTimeType>()

UtcTime:16:49 ...当地时间:18:49

  • PreUpdate:2015-03-30T16:49:00.2754289Z Tick:635633309402754289种类:Utc
  • 已保存:2015-03-30T16:49:00.2754289Z Tick:635633309402754289种类:Utc
  • 已加载:2015-03-30T16:49:00.0000000 Tick:635633309400000000种类:未指定

使用此解决方案,我会丢失毫秒,而DateTime类型也是未指定的。

Mappingtype:CustomType<UtcDateTimeType>()

UtcTime:17:01 ... LocalTime:19:01

  • PreUpdate:2015-03-30T17:01:32.9663859Z Tick:635633316929663859种类:Utc
  • 已保存:2015-03-30T17:01:32.9663859Z Tick:635633316929663859种类:Utc
  • 已加载:2015-03-30T19:01:32.0000000Z Tick:635633388920000000种类:Utc

使用这个解决方案,我松开了毫秒,DateTime类是utc,但它是错误的时间,它应该是17:01 ....

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

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

Mappingtype:CustomType<TimestampType>()

UtcTime:17:21 ......当地时间:19:21

  • PreUpdate:2015-03-30T19:21:44.7938077 + 02:00 Tick:635633401047938077种类:本地
  • 已保存:2015-03-30T19:21:44.7938077 + 02:00勾选:635633401047938077种类:本地
  • 已加载:2015-03-30T19:21:44.7938077 Tick:635633401047938077种类:未指定

使用此解决方案,我有毫秒,DateTime类型未指定,加载时间不是utc。

Mappingtype:CustomType<DateTimeType>()

UtcTime:17:19 ...当地时间:19:19

  • PreUpdate:2015-03-30T19:19:27.3114047 + 02:00 Tick:635633399673114047种类:本地
  • 已保存:2015-03-30T19:19:27.3114047 + 02:00勾选:635633399673114047种类:本地
  • 已加载:2015-03-30T19:19:27.0000000 Tick:635633399670000000种类:未指定

使用此解决方案,我会丢失毫秒,DateTime类型也是未指定的,加载时间不是utc。

Mappingtype:CustomType<UtcDateTimeType>()

UtcTime:17:14 ......当地时间:19:14

  • PreUpdate:2015-03-30T19:14:31.3030033 + 02:00 Tick:635633396713030033种类:本地
  • 已保存:2015-03-30T19:14:31.3030033 + 02:00勾选:635633396713030033种类:本地
  • 已加载:2015-03-30T21:14:31.0000000Z Tick:635633468710000000种类:Utc

使用这个解决方案,我放松了毫秒,DateTime种类是utc,但这是错误的时间,它应该是17:14 ....

所以我有一些问题:

  1. 为什么NHibernate加载本地时间但是加上utc(UtcDateTimeType和o.Created = DateTime.UtcNow)
  2. 最好在孔应用程序和UI本地时间使用utc,或者在任何地方使用本地时间,并在数据库中保存时间。
  3. 我还创建了一个自己的映射:

    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() + '\'';
            }
        }
    }
    

3 个答案:

答案 0 :(得分:3)

  1. 将日期时间加载为UTC的行为,我认为使用类型UtcDateTimeType时的预期行为是假设时间设置为UTC,并且当获取它时也将其视为UTC,
  2. 如果您执行快速测试,

        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会是更好的选择,尤其是当用户处于时区范围内时。但是,如果用户位于同一时区,则没有点转换,因为它很难诊断错误,特别是查询数据库,因为您必须始终转换值。
    2. 作为精确解决此问题的方法,您可以添加像这样的自定义类型

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

答案 1 :(得分:2)

感谢您的回答。使用您的自定义类型版本时,我通常在加载日期时间时出错。问题是这段代码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="http://stackoverflow.com/questions/29352719/save-and-load-utc-datetime-with-nhibernate"/>
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);
    }

答案 2 :(得分:0)

我遇到了同样的问题,在我的情况下,我试图将所有内容都映射为try/catch,以Utc的形式来回传递,但是时间又回​​到了UTC,但转换为本地时间。答案既简单又烦人。您必须tell the SQLite engine not to convert DateTimes for you by adding a parameter to the connection string

UtcDateTimeType

完成此操作后,我的应用程序的行为更加明智。