来自 SQL 服务器的 DateTime 时区

DateTime from SQL Server with timezone

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

但是在创建数据集并将其转换为 XML 时,我在 DateTime 字段中得到了时区 (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 字段,它可能并不总是有值。我将其转换为字符串,然后将其转换回 DateTime。 dt 的值然后被序列化为 json。我得到的输出是 2015-02-17T00:00:00 而不是 2015-02-17T00:00:00+11:00。我检查了 dt 的时区,它是 Unspecified

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

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

此外,有没有办法使用 DataReader 获取带时区的日期时间?

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

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

DateTimeOffset resolution in c# and SQL Server

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

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

DataSet is the hardest technology of the three to work around this problem. Some options:
1. Change the column types to be Int64 or 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.
  1. Make all machines use the same time zone.

  2. Use Remoting to serialize the DataSet in binary form. This also has performance benefits. This KB article has an example.

  3. If you have a chance to pre-process the XML before it is sent out, you can manually strip out the time zone offset out of the XML text. For example, a typical XML date and time looks like this: “2005-01-28T03:14:42.0000000-07:00”. You can use a Regex to remove the “-07:00”. You do not need to re-inject anything on the other end, as no adjustment is made if there is no time zone information. Do not try to replace the time zone offset with “Z” or “+00:00”. While technically a more correct representation, the existence of time zone information will cause the serializer to do an extra conversion to local.

This is the most difficult situation, because all of these work-arounds have problems. Option (1) involves bypassing the database type system. Option (2) has a reliability caveat explained below. I would actually recommend (4) or (5) for this technology.

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

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

正确的咒语如下:

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

虽然 reader["dte_time"] 的 return 类型是 object,但该对象包含 DateTime。如果设置断点,您会看到 DateTime 已经存在。你只需要 cast it so it can be assigned to a DateTime variable. This is called 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 服务器数据类型和 .NET 数据类型之间的其他类型映射 in the chart here

现在关于时区,那是一个不同的问题。首先,了解 DateTime 不保留时区。它只知道分配给它的 DateTimeKind 。默认情况下,种类是 Unspecified,这实际上意味着 "I don't know; it could be anything".

也就是说,不同的协议有不同的要求。 JSON 没有预定义的日期格式,但最常见的约定(和最佳做法)是将日期存储在 ISO8601 format, which is YYYY-MM-DDTHH:mm:ss. Time zone information is optional, and will usually not be included when the .Kind of a DateTime is DateTimeKind.Unspecified. If it were Utc, then you would see a Z at the end, and if it were Local, then you would see an offset of the local time zone, such as +11:00. That is, whatever offset is appropriate for that time zone, at that particular moment. An offset is not the same thing as a "time zone", because different offset could apply within the same time zone at different times - usually to daylight saving time.

XML有点不同。 .NET 中的大多数 XML 序列化将使用 W3C XML Schema specification, and will map a DateTime to an xsd:dateTime 类型。具体如何呈现将取决于 Kind.

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

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

理想情况下,您可以在 SQL 服务器中使用 datetimeoffset 类型,在 .NET 中使用 DateTimeOffset 类型。这避免了 "kind" 问题,并在 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,则它根本无法呈现。你只是得到一个空节点,比如 <Value/>.

然而,即使说了这么多,您还是说您使用的是 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是过去的遗物。在现代开发中,应该很少需要在日常代码中使用它。如果可能的话,我会认真考虑探索其他选择。