关于c#:在数据库中将日期/时间存储为UTC

Storing date/times as UTC in database

我将数据库中的日期/时间存储为UTC,并根据特定时区将其在我的应用程序中计算回本地时间。比方说我有以下日期/时间:

01/04/2010 00:00

说这是一个国家,例如英国观察夏令时(夏令时),在这个特定的时间,我们在夏令时。当我将此日期转换为UTC并将其存储在数据库中时,它实际存储为:

31/03/2010 23:00

由于日期将调整为夏令时的-1小时。当您在提交时观察DST时,此工作正常。但是,当时钟调整回来时会发生什么?当我从数据库中提取该日期并将其转换为本地时间时,特定日期时间将被视为31/03/2009 23:00,而实际上它被处理为01/04/2010 00:00

如果我错了,请纠正我但是在存储UTC时间时这不是一个缺陷吗?

时区转换的示例

基本上我正在做的是存储信息提交到我的系统的日期/时间,以便允许用户进行范围报告。这是我存储日期/时间的方式:

1
2
3
4
5
public DateTime LocalDateTime(string timeZoneId)
{
    var tzi = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
    return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi).ToUniversalTime().ToLocalTime();
}

存储为UTC:

1
2
var localDateTime = LocalDateTime("AUS Eastern Standard Time");
WriteToDB(localDateTime.ToUniversalTime());

您不会根据您当前是否正在观察它们来调整DST更改的日期 - 您可以根据在您描述的瞬间是否观察到DST来调整它。因此,在1月份的情况下,您不会应用调整。

然而,有一个问题 - 一些当地时间是模棱两可的。例如,2010年10月31日凌晨1:30在英国可以代表UTC 01:30或UTC 02:30,因为时钟从早上2点回到凌晨1点。您可以从UTC中表示的任何时刻到当时将显示的当地时间,但操作不可逆。

同样地,你很可能有一个从未发生过的当地时间 - 例如,2010年3月28日凌晨1:30在英国没有发生 - 因为凌晨1点,时钟跳到凌晨2点。

它的长短是因为如果你想要及时表示,你可以使用UTC并获得明确的表示。如果您尝试在特定时区中表示时间,则需要时区本身(例如欧洲/伦敦)以及当前的UTC表示或本地日期和时间以及该特定时间的偏移量(消除DST过渡的歧义)。另一种方法是仅存储UTC及其偏移量;这允许你告诉当时的当地时间,但这意味着你无法预测一分钟之后的当地时间,因为你真的不知道时区。 (这基本上是DateTimeOffset存储的。)

我们希望在Noda Time中使这个相当容易处理,但你仍然需要意识到这是一种可能性。

编辑:

您显示的代码不正确。这就是原因。我已经改变了代码的结构,使其更容易看到,但你会看到它正在执行相同的调用。

1
2
3
4
var tzi = TimeZoneInfo.FindSystemTimeZoneById("AUS Eastern Standard Time");
var aussieTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi);
var serverLocalTime = aussieTime.ToLocalTime();
var utcTime = serverLocalTime.ToUniversalTime();

那么,让我们现在考虑一下 - 当地时间是13:38(UTC + 1,在伦敦),12:38 UTC,22:39在悉尼。

您的代码将给出:

1
2
3
aussieTime = 22:39 (correct)
serverLocalTime = 23:39 (*not* correct)
utcTime = 22:39 (*not* correct)

你不应该在TimeZoneInfo.ConvertTimeFromUtc的结果上调用ToLocalTime - 它会假设它是在UTC DateTime上调用的(除非它实际上有DateTimeKind.Local,在这种情况下它不会)。

因此,如果您在这种情况下准确保存22:39,则无法准确地以UTC格式保存当前时间。


您尝试将日期和时间存储为UTC是件好事。通常最好和最容易将UTC视为实际的日期和时间,当地时间只是假名。如果您需要对日期/时间值进行任何数学计算以获得时间跨度,那么UTC绝对是至关重要的。我通常在内部操作日期为UTC,并且只在向用户显示值时转换为本地时间(如果有必要)。

您遇到的错误是您错误地将本地时区分配给日期/时间值。 1月在英国将当地时间解释为夏令时区是不正确的。您应该使用在时间值表示的时间和位置生效的时区。

将时间转换回来进行显示完全取决于系统的要求。您可以将时间显示为用户的本地时间,也可以显示为数据的源时间。但无论哪种方式,夏令时/夏令时调整都应适当适用于目标时区和时间。


TimeZoneInfo.ConvertTimeFromUtc()方法将解决您的问题:

1
2
3
4
5
6
7
8
9
10
11
12
using System;

class Program {
  static void Main(string[] args) {
    DateTime dt1 = new DateTime(2009, 12, 31, 23, 0, 0, DateTimeKind.Utc);
    TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
    Console.WriteLine(TimeZoneInfo.ConvertTimeFromUtc(dt1, tz));
    DateTime dt2 = new DateTime(2010, 4, 1, 23, 0, 0, DateTimeKind.Utc);
    Console.WriteLine(TimeZoneInfo.ConvertTimeFromUtc(dt2, tz));
    Console.ReadLine();
  }
}

输出:

1
2
12/31/2009 11:00:00 PM
4/2/2010 12:00:00 AM

您需要.NET 3.5或更高版本,并在保持历史夏令时更改(Vista,Win7或Win2008)的操作系统上运行。


您可以通过存储转换为UTC时使用的特定偏移来解决此问题。在您的示例中,您将日期存储为类似的内容

1
31/12/2009 23:00 +0100

向用户显示此信息时,您可以根据需要使用相同的偏移量或其当前的本地偏移量。

这种方法也有其自身的问题。时间是一件混乱的事情。


这是一个巨大的缺陷,但它不是以UTC存储时间的缺陷(因为这是唯一合理的事情 - 存储本地时间总是一场灾难)。这是一个缺陷,是夏令时的概念。
真正的问题是时区信息会发生变化。 DST规则是动态和历史性的。他们在2010年从美国开始的DST在2000年开始时并不相同。直到最近,Windows甚至没有包含这些历史数据,所以基本上不可能正确地做事。您必须使用tz数据库才能正确完成。现在我只是用谷歌搜索它,似乎.NET 3.5和Vista(我也假设Windows 2008)已经做了一些改进,System.TimeZoneInfo实际上处理历史数据。看看这个。

但基本上DST必须去。


Correct me if I am wrong but isn't
this a bit of a flaw when storing
times as UTC?

是的。此外,调整的天数将是23或25小时,因此前一天的成语同时是当地时间 - 一年2天24小时是错误的。

该修复程序正在挑选一个标准并坚持使用它。将日期存储为UTC并显示为本地是非常标准的。只是不要使用本地计算的快捷方式(+ - somthing)=新时间,你没事。