Save and load Utc DateTime with NHibernate

三世轮回 提交于 2020-07-18 05:54:11

问题


I have problems with saving DateTime to an SQL Lite data base. (perhaps also with MS SQL)

I want to save a DateTime in UTC time with NHibernate to the database and load it from the database. We work in the hole application with utc time and when we present the time on the ui, we change it to local time.

I read a lot about DateTime and NHibernate:

  • http://jameskovacs.com/2011/01/26/datetime-support-in-nhibernate/
  • http://www.milkcarton.com/blog/2007/01/19/NHibernate+DateTime+And+UTC.aspx
  • Rehydrating fluent nhibernate configured DateTime as Kind Utc rather than Unspecified
  • https://nhibernate.jira.com/browse/NH-2520

But nothing works. Some expample:

PreUpdate: is before saving the entity.

Saved: is the saved object after saving with nhibernate (repo.save(o);).

Loaded: when I load the entity from the repository by id.

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

Mappingtype: CustomType<TimestampType>()

UtcTime: 16:44... LocalTime: 18:44

  • PreUpdate: 2015-03-30T16:44:35.7636679Z Tick: 635633306757636679 Kind:Utc
  • Saved: 2015-03-30T16:44:35.7636679Z Tick: 635633306757636679 Kind:Utc
  • Loaded: 2015-03-30T18:44:35.7636679 Tick: 635633378757636679 Kind:Unspecified

The problem here is, when I reload the object by id, the new object has the time 18... (+2h) instead of 16.... and the DateTime kind is Unspecified.

Mappingtype: CustomType<DateTimeType>()

UtcTime: 16:49... LocalTime: 18:49

  • PreUpdate: 2015-03-30T16:49:00.2754289Z Tick: 635633309402754289 Kind:Utc
  • Saved: 2015-03-30T16:49:00.2754289Z Tick: 635633309402754289 Kind:Utc
  • Loaded: 2015-03-30T16:49:00.0000000 Tick: 635633309400000000 Kind:Unspecified

With this solution, I loose the milliseconds and the DateTime kind is also Unspecified.

Mappingtype: CustomType<UtcDateTimeType>()

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

  • PreUpdate: 2015-03-30T17:01:32.9663859Z Tick: 635633316929663859 Kind:Utc
  • Saved: 2015-03-30T17:01:32.9663859Z Tick: 635633316929663859 Kind:Utc
  • Loaded: 2015-03-30T19:01:32.0000000Z Tick: 635633388920000000 Kind:Utc

With this solution, I loose the milliseconds, the DateTime kind is utc but it is the wrong time, it should be 17:01....

So another idea was to use in the application only DateTime.Now and save the utc time in the database. Some example:

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

Mappingtype: CustomType<TimestampType>()

UtcTime: 17:21... LocalTime: 19:21

  • PreUpdate: 2015-03-30T19:21:44.7938077+02:00 Tick: 635633401047938077 Kind:Local
  • Saved: 2015-03-30T19:21:44.7938077+02:00 Tick: 635633401047938077 Kind:Local
  • Loaded: 2015-03-30T19:21:44.7938077 Tick: 635633401047938077 Kind:Unspecified

With this solution, I have the milliseconds, the DateTime kind is Unspecified and the loaded time is not utc.

Mappingtype: CustomType<DateTimeType>()

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

  • PreUpdate: 2015-03-30T19:19:27.3114047+02:00 Tick: 635633399673114047 Kind:Local
  • Saved: 2015-03-30T19:19:27.3114047+02:00 Tick: 635633399673114047 Kind:Local
  • Loaded: 2015-03-30T19:19:27.0000000 Tick: 635633399670000000 Kind:Unspecified

With this solution, I loose the milliseconds, the DateTime kind is also Unspecified and the loaded time is not utc.

Mappingtype: CustomType<UtcDateTimeType>()

UtcTime: 17:14... LocalTime: 19:14

  • PreUpdate: 2015-03-30T19:14:31.3030033+02:00 Tick: 635633396713030033 Kind:Local
  • Saved: 2015-03-30T19:14:31.3030033+02:00 Tick: 635633396713030033 Kind:Local
  • Loaded: 2015-03-30T21:14:31.0000000Z Tick: 635633468710000000 Kind:Utc

With this solution, I loose the milliseconds, the DateTime kind is utc but it is the wrong time, it should be 17:14....

So I have some questions:

  1. Why does NHibernate loads the local time but with kind utc (UtcDateTimeType and o.Created=DateTime.UtcNow)
  2. Is it better to use utc in the hole application and in UI localtime or use localtime everywhere and save the time utc at the database.

I've also created an own mapping:

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:


  1. The behavior of loading the date time as UTC, I believe is the intended behavior when using the type UtcDateTimeType is to assume the time set is in UTC and when fetch it also consider it as UTC,

If you do following quick test,

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

Output

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

even though I have persisted the 12:09:41 as local, when I fetch it back it's UTC, and the same time as it assumes everything happens in UTC. This test was done with the SQLServer database.

If I repeat the same test with SQLite, output is,

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

I can see one hour unexplained difference here (since difference between local time and UTC where the test is conducted is 11 hours, this does not explain it. Only explanation I can think of is that there might be some sort of bug in the SQLLite dialect.

  1. I think saving UTC would be the better option, especially when users are in range of time zones. However if the users are in the same timezone, there is no point converting, as it makes it difficult to diagnose errors especially querying database as you have to always convert the values.

As a fix to this problem with precision you could add a custom type like this,

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



回答2:


Thank you for your answer. With your version of the custom type, I had usually a wrong time when I've loaded the datetime. The problem is this code base.Get(rs, index). When I execute this method, it converts the utc time from the database to a wrong DateTime. My solution is now, to save the time in utc, but before I change the DateTimeKind to Unspecified and store the value in the data base (SQLite). The result is a time without Z at the end. When I load the time, I change the DateTimeKind after loading the DateTime and everything works.

The following code solve my problems:

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

What do you think about these 2 Methods? Do I need these? Should I use Round(DateTime.UtcNow...? When is FromStringValue needed?

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



回答3:


I was having this same problem, in my case I was trying to round-trip everything as Utc, with fields mapped as UtcDateTimeType, but times were coming back marked as UTC but converted to local time. The answer is both simple and annoying. You must 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";
            //                                              ^^^^^^^^^^^^^^^^
            ...

After doing this my application behaved MUCH more sensibly.



来源:https://stackoverflow.com/questions/29352719/save-and-load-utc-datetime-with-nhibernate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!