来自带有时区的 SQL Server 的 DateTime

Posted

技术标签:

【中文标题】来自带有时区的 SQL Server 的 DateTime【英文标题】:DateTime from SQL Server with timezone 【发布时间】:2015-05-20 15:49:45 【问题描述】:

当我执行查询并使用 DataReader 访问值并将其转换为字符串时,我没有得到 TimeZone(2015-02-17T00:00:00)。

但是在创建 DataSet 并将其转换为 XML 时,我在 DateTime 字段中获得了 TimeZone (2015-02-17T00:00:00+11:00)。

从数据读取器中检索数据的代码是var dateTime = reader["dte_tme"].ToString(),它产生17/02/2015 12:00:00 AM(没有TimeZone)。

string dateTime = reader["dte_tme"].ToString();
DateTime dt = Convert.ToDateTime(dateTime);

所以我知道“dte_tme”字段是一个日期时间字段,它可能并不总是有值。我将其转换为字符串,然后将其转换回 DateTime。 dt 的值随后被序列化为 json。我得到的输出是2015-02-17T00:00:00 而不是2015-02-17T00:00:00+11:00。我检查了dt 的时区,它是Unspecified

我从 DataSet 中的 XML 创建的 DateTime 对象的 TimeZone 为 Local,它序列化为 2015-02-17T00:00:00+11:00

为什么会出现这种不一致?

另外,有没有办法使用 DataReader 获取带有 TimeZone 的 DateTime?

我的最终目标是以 ISO 8601 格式序列化 DateTime 字段。

【问题讨论】:

你用什么把DataSet转换成xml? @KeithPayne DataSet ds = GetDataSet(blah, blah); var xml = dataSet.GetXml(); 【参考方案1】:

这是一个很常见的反模式:

string dateTime = reader["dte_tme"].ToString();
DateTime dt = Convert.ToDateTime(dateTime);

正确的咒语如下:

DateTime dt = (DateTime) reader["dte_tme"];

虽然reader["dte_time"] 的返回类型是object,但该对象包含DateTime。如果设置断点,您会看到DateTime 已经存在。你只需要cast 它就可以分配给DateTime 变量。这称为unboxing

如果 SQL 数据库中的 datetime 列可以为空,那么您应该像这样进行测试:

DateTime? dt = reader["dte_tme"] == DBNull.Value ? null : (DateTime) reader["dte_tme"];

或者有时候你会看到这样,同样可以接受:

DateTime? dt = reader["dte_tme"] as DateTime?;

在从数据库中检索时,绝对需要在任何时候将其视为字符串。如果它是数据库中的 datetime,那么它是 C# 中的 DateTime

从数据读取器中提取数据时,您应该使用强制转换操作,即使是其他数据类型,例如整数、小数、甚至字符串。您可以查看 SQL Server 数据类型和 .NET 数据类型in the chart here 之间的其他类型映射。

现在关于时区,这是一个不同的问题。首先,了解DateTime 不保留时区。它只知道分配给它的DateTimeKind。默认情况下,类型是Unspecified,这基本上意味着“我不知道;它可能是任何东西”。

也就是说,不同的协议有不同的要求。 JSON 没有预定义的日期格式,但最常见的约定(和最佳实践)是以ISO8601 格式存储日期,即YYYY-MM-DDTHH:mm:ss。时区信息可选,当DateTime.KindDateTimeKind.Unspecified 时,通常不会包含时区信息。如果是Utc,那么最后你会看到Z,如果是Local,那么你会看到本地时区的偏移量,比如+11:00。也就是说,在那个特定的时刻,适合那个时区的任何偏移量。偏移量与“time zone”不同,因为不同的偏移量可能适用于同一时区的不同时间 - 通常适用于 daylight saving time。

XML 有点不同。 .NET 中的大部分 XML 序列化将使用 W3C XML Schema 规范,并将 DateTime 映射到 xsd:dateTime 类型。具体如何渲染将取决于Kind

对于DateTimeKind.Unspecified,它将不包含偏移量。 对于DateTimeKind.Utc,它将附加一个Z 对于DateTimeKind.Local,它将附加本地偏移量

当您在数据集中查看Kind 时,您问为什么LocalLocal?那是因为DataSet 有一个丑陋的行为,即假设所有时间都是本地时间。它基本上忽略了.Kind 属性并假定DateTimeKind.Local 的行为。这是一个长期存在的错误。

理想情况下,您应该在 SQL Server 中使用 datetimeoffset 类型,在 .NET 中使用 DateTimeOffset 类型。这避免了“种类”问题,并在 JSON 中很好地序列化(当您使用像 JSON.NET 这样的现代序列化程序时)。然而,在 XML 中,它应该被映射到 xsd:dateTime 并像本地 DateTime 一样呈现,只是使用正确的偏移量。然而,它最终看起来像这样:

<Value xmlns:d2p1="http://schemas.datacontract.org/2004/07/System">
    <d2p1:DateTime>2015-03-18T03:34:11.3097587Z</d2p1:DateTime>
    <d2p1:OffsetMinutes>-420</d2p1:OffsetMinutes>
</Value>

那是DataContractXmlSerializer。如果您使用XmlSerializer,则根本无法渲染。你只会得到一个空节点,比如&lt;Value/&gt;

但是,尽管说了这么多,但您说您使用的是DataSet,这伴随着它自己的一组行为。不利的一面是,它会假设所有DateTime 值都有DateTimeKind.Local——即使它们没有,正如我上面提到的。考虑以下几点:

DataTable dt = new DataTable();
dt.Columns.Add("Foo", typeof (DateTime));

dt.Rows.Add(new DateTime(2015, 1, 1, 0, 0, 0, DateTimeKind.Unspecified));
dt.Rows.Add(new DateTime(2015, 1, 1, 0, 0, 0, DateTimeKind.Local));
dt.Rows.Add(new DateTime(2015, 1, 1, 0, 0, 0, DateTimeKind.Utc));

DataSet ds = new DataSet();
ds.Tables.Add(dt);
string xml = ds.GetXml();

Debug.Write(xml);

这是我运行它时的输出(在美国太平洋时区):

<NewDataSet>
  <Table1>
    <Foo>2015-01-01T00:00:00-08:00</Foo>
  </Table1>
  <Table1>
    <Foo>2015-01-01T00:00:00-08:00</Foo>
  </Table1>
  <Table1>
    <Foo>2015-01-01T00:00:00-08:00</Foo>
  </Table1>
</NewDataSet>

不过,好消息是DateTimeOffset 的值要好一些:

DataTable dt = new DataTable();
dt.Columns.Add("Foo", typeof(DateTimeOffset));

dt.Rows.Add(new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.FromHours(11)));
dt.Rows.Add(new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero));
dt.Rows.Add(new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.FromHours(-3)));

DataSet ds = new DataSet();
ds.Tables.Add(dt);
string xml = ds.GetXml();

Debug.Write(xml);

输出:

<NewDataSet>
  <Table1>
    <Foo>2015-01-01T00:00:00+11:00</Foo>
  </Table1>
  <Table1>
    <Foo>2015-01-01T00:00:00Z</Foo>
  </Table1>
  <Table1>
    <Foo>2015-01-01T00:00:00-03:00</Foo>
  </Table1>
</NewDataSet>

在大多数情况下,这是正确的,尽管技术上它应该使用+00:00 而不是Z 序列化第二个,但这在实践中并不重要。

我想说的最后一件事是,总的来说,DataSet 是过去的遗物。在现代开发中,应该很少需要在日常代码中使用它。如果可能,我会认真考虑探索其他选择。

【讨论】:

强制转换值将引发异常,因为并非所有记录都在字段中具有值。相反,我将字符串解析为 DateTime 并使用 DateTimeKind 作为 Local 创建了另一个 DateTime 并将其转换为通用时间,这是我需要通过 json 序列化的时间。 我更新了答案以包括如何处理空值。您永远不必去那里的字符串。另外,如果您想更改种类,只需使用DateTime.SpecifyKind。你也不必去那里串起来。关于 json - 你没有真正解释你在那里做什么或提供任何代码示例,所以我不确定如果没有其他信息我能提供帮助。 @Matt - 可能的替代方案:DateTime? dt = reader["dte_tme"] == DBNull.Value ?空:(日期时间)阅读器[“dte_tme”];是:日期时间? dt = reader["dte_tme"] as DateTime?;【参考方案2】:

似乎DataSet.GetXml() 和DataSet 的其他xml 编写方法都有一个丑陋的问题,即假设datetime 的值是本地时间。它使用执行代码的机器的时区设置。

修复它的 MS 解决方法同样丑陋。来自http://blogs.msdn.com/b/bclteam/archive/2005/03/07/387677.aspx:

DataSet 是三者中最难解决此问题的技术。 一些选项: 1.将列类型改为Int64或String

2. Call DateTime.ToLocalTime on the DateTime before putting it in the DataSet and call DateTime.ToUniversalTime after taking it out. This will  effectively “cancel out” the adjustment, and can be used whether you are dealing with a whole date or a UTC time.

    让所有机器使用相同的时区。

    使用 Remoting 以二进制形式序列化 DataSet。这也具有性能优势。此知识库文章有一个示例。

    如果您有机会在发送 XML 之前对其进行预处理,您可以手动从 XML 文本中去除时区偏移量。例如,典型的 XML 日期和时间如下所示:“2005-01-28T03:14:42.0000000-07:00”。您可以使用正则表达式删除“-07:00”。您不需要在另一端重新注入任何内容,因为如果没有时区信息,则不会进行任何调整。不要尝试用“Z”或“+00:00”替换时区偏移。虽然从技术上讲是一种更正确的表示,但时区信息的存在会导致序列化程序对本地进行额外的转换。

这是最困难的情况,因为所有这些变通方法都有问题。选项 (1) 涉及绕过数据库类型系统。选项 (2) 有一个可靠性警告,如下所述。对于这项技术,我实际上会推荐 (4) 或 (5)。

【讨论】:

另一种从 DataSet XML 序列化中删除时区的方法(适用于 .net 2.0 或更高版本):***.com/questions/6932071/… 有问题的 DataSet 实际上是由 DataAdapter 使用 dataAdapter.Fill(dataSet) 方法填充的。我使用DateTime.SpecifyKind 设置了DateTime 对象的DateTimeKind,然后通过调用dateTime.ToUniversalTime() 将其转换为UTC 类型。【参考方案3】:

时区信息不存储在DateTime 数据类型中,它存储在DateTimeOffset 数据类型中。当您转换为 DateTime 时,您正在从数据中剥离时区。尝试改用DateTimeOffset。有关更多信息,请参阅此线程:

DateTimeOffset resolution in c# and SQL Server

【讨论】:

以上是关于来自带有时区的 SQL Server 的 DateTime的主要内容,如果未能解决你的问题,请参考以下文章

Oracle 将带有时区的 TIMESTAMP 转换为 DATE

SQL DATE 与 java.sql.Date 中的时区

来自字符串的日期时间时区

如何在没有时区的情况下解析 ISO8061 至今

来自带有时区和夏令时的字符串的 Qt QDateTime

[SQL SERVER][Memo]全时区转换