您的位置:首页 > 数据库 > Mongodb

关于使用 C# 读写 MongoDB 时涉及 DateTime 的问题

2019-04-20 14:58 92 查看

       使用 C# 读写 MongoDB 时,DateTime 常常在 Unspecified、Local 和 Utc 之间转换,搞不清楚的话很容易弄错。最近写程序弄错了两次,数据老是重复,刚开始一直找不到问题,浪费了两天时间,坑爹啊。今天查资料、写测试、找源码,整了两个多小时,终于搞清楚了。

关于 DateTimeKind

// DateTimeKind.Local
DateTime td = DateTime.Today;

// DateTimeKind.Unspecified
DateTime dt1 = new DateTime(1999, 1, 1);
// 输出:dt1:1999-01-01 00:00:00, Ticks:630507456000000000
Console.WriteLine("dt1:{0}, Ticks:{1}", dt1, dt1.Ticks);

DateTime dt2 = new DateTime(1999, 1, 1, 0, 0, 0, DateTimeKind.Local);
// 输出:dt2:1999-01-01 00:00:00, Ticks:630507456000000000
Console.WriteLine("dt2:{0}, Ticks:{1}", dt2, dt2.Ticks);

DateTime dt3 = new DateTime(1999, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// 输出:dt3:1999-01-01 00:00:00, Ticks:630507456000000000
Console.WriteLine("dt3:{0}, Ticks:{1}", dt3, dt3.Ticks);

Console.WriteLine(dt1 == dt2); // 输出:True
Console.WriteLine(dt1 == dt3); // 输出:True
Console.WriteLine(dt2 == dt3); // 输出:True

       查看 DateTime 源代码可知,虽然三个时间的 DateTimeKind 不同,但 Ticks 值是一样的,而 DateTime 的默认比较器只比较了 Ticks 值,没有转换到统一的标准。

       关于 DateTimeKind 转换和直接指定的问题参考 System.DateTimeKind 的用法

mongo-csharp-driver(2.8)实现的时间序列化类

1. DateTimeSerializer

       默认无参构造函数最终调用的是私有构造函数

DateTimeSerializer(bool dateOnly, DateTimeKind kind, BsonType representation)

       参数值为 False, DateTimeKind.Utc,BsonType.DateTime。

  • 序列化

DateTime utcDateTime;
if (_dateOnly)
{
if (value.TimeOfDay != TimeSpan.Zero)
{
throw new BsonSerializationException("TimeOfDay component is not zero.");
}
utcDateTime = DateTime.SpecifyKind(value, DateTimeKind.Utc); // not ToLocalTime
}
else
{
utcDateTime = BsonUtils.ToUniversalTime(value);
}
var millisecondsSinceEpoch = BsonUtils.ToMillisecondsSinceEpoch(utcDateTime);

switch (_representation)
{
case BsonType.DateTime:
bsonWriter.WriteDateTime(millisecondsSinceEpoch);
break;
...
}

       可见,无论何时 MongoDB 存储的都是 Utc 时间。而默认情况下,更确切的说是代码时间转换为 Utc 时间后的毫秒级的 Unix 时间刻度。

  • 反序列化

switch (bsonType)
{
case BsonType.DateTime:
// use an intermediate BsonDateTime so MinValue and MaxValue are handled correctly
value = new BsonDateTime(bsonReader.ReadDateTime()).ToUniversalTime();
break;
...
}

if (_dateOnly)
{
if (value.TimeOfDay != TimeSpan.Zero)
{
throw new FormatException("TimeOfDay component for DateOnly DateTime value is not zero.");
}
value = DateTime.SpecifyKind(value, _kind); // not ToLocalTime or ToUniversalTime!
}
else
{
switch (_kind)
{
case DateTimeKind.Local:
case DateTimeKind.Unspecified:
value = DateTime.SpecifyKind(BsonUtils.ToLocalTime(value), _kind);
break;
case DateTimeKind.Utc:
value = BsonUtils.ToUniversalTime(value);
break;
}
}

       可见,默认情况下,MongoDB 中读取出来的 DateTime 是 Utc 时间。要想获取到本地时间,可以为 DateTime 注册一个自定义的序列化接口。可以是自己实现的序列化类,也可以是内置的 DateTimeSerializer 类 + 构造参数 DateTimeKind.Local

BsonSerializer.RegisterSerializer(typeof(DateTime), new DateTimeSerializer(DateTimeKind.Local));

2. DateTimeOffsetSerializer

       再看看传说中的 DateTimeOffset。DateTimeOffset 没有 DateTimeKind 成员,只存储了 Ticks,可以说是完全的 Utc 时间。默认无参构造函数调用的是

DateTimeOffsetSerializer(BsonType representation)

       参数值为 BsonType.Array。

  • 序列化

switch (_representation)
{
case BsonType.Array:
bsonWriter.WriteStartArray();
bsonWriter.WriteInt64(value.Ticks);
bsonWriter.WriteInt32((int)value.Offset.TotalMinutes);
bsonWriter.WriteEndArray();
break;
...
}
  • 反序列化

switch (bsonType)
{
case BsonType.Array:
bsonReader.ReadStartArray();
ticks = bsonReader.ReadInt64();
offset = TimeSpan.FromMinutes(bsonReader.ReadInt32());
bsonReader.ReadEndArray();
return new DateTimeOffset(ticks, offset);
...
}

       DateTimeOffsetSerializer 的存取逻辑看起来简单粗暴得多。

其他文档数据库的时间序列化类

       其他 BSON 文档数据库对 DateTime 的存取逻辑也类似,如 LiteDB(4.1.4)的序列化类。

1. BsonSerializer

  • 序列化

switch (value.Type)
{
...
case BsonType.DateTime:
writer.Write((byte)0x09);
this.WriteCString(writer, key);
var date = (DateTime)value.RawValue;
// do not convert to UTC min/max date values - #19
var utc = (date == DateTime.MinValue || date == DateTime.MaxValue) ? date : date.ToUniversalTime();
var ts = utc - BsonValue.UnixEpoch;
writer.Write(Convert.ToInt64(ts.TotalMilliseconds));
break;
...
}
  • 反序列化

BsonDocument Deserialize(byte[] bson, bool utcDate = false)
...
...
else if (type == 0x09) // DateTime
{
var ts = reader.ReadInt64();

// catch specific values for MaxValue / MinValue #19
if (ts == 253402300800000) return DateTime.MaxValue;
if (ts == -62135596800000) return DateTime.MinValue;

var date = BsonValue.UnixEpoch.AddMilliseconds(ts);

return _utcDate ? date : date.ToLocalTime();
}
...

       LiteDB 的存取逻辑相对简单,序列化逻辑与 MongoDB 的默认逻辑一致,但读取逻辑默认取到的是本地时间。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  MongoDB