MongoDB
对于时间类型的数据,Mongo中使用BSON标准的时间类型,64位二进制表示的自 Unix 纪元以来的 UTC 毫秒数。所以mongo中的日期本质是一个Int64的数字(UTC时区),所以无法从数据库根本上改变日期的时区。不过,Mongo内置的方法以及各个语言版本的driver都是有时区设置,已实现对开发者透明的时区转换。
C# Driver(2.16.1)
C#中的时间 (DateTime)
DateTime结构体应该是我们用的最多的一种变量类型,基础的用法也比较简单,需要注意的只有一点,就是DateTime是包含时区信息的。这一点是我们可能忽略的。如:
DateTime now = DateTime.Now;
var a = now.Kind;
var b = (a == DateTimeKind.Local);
上面示例中 b为true。注意DateTimeKind属性,是因为我们使用DateTime.Parse时如果时间字符串中没有时区信息,则转换成的时间Kind属性为Unspecified(未指定)。这种情况下,使用第三方库时就无法确定第三方库是按什么时区处理此时间,造成不确定。或者使用.ToLocalTime() 时没有在意自动加了8小时造成问题。如下:
var t = DateTime.Parse("2022-07-08 11:11:11");
b = (t.Kind == DateTimeKind.Local);//b==false
t = t.ToLocalTime();//2022-07-08 19:11:11
//带时区信息的时间
var nt = DateTime.Parse("2022-07-08 11:11:11 +08");//+8区
b = (nt.Kind == DateTimeKind.Local);//b==true
nt = nt.ToLocalTime();//2022-07-08 11:11:11
所以,在使用DateTime类型的数据时需要注意下时区信息。同时可以使用DateTime.SpecifyKind来指定时区信息。如下:
var t = DateTime.Parse("2022-07-08 11:11:11");
b = (t.Kind == DateTimeKind.Local);//false
t = DateTime.SpecifyKind(t, DateTimeKind.Local);//修改时区
b = (t.Kind == DateTimeKind.Local);//true
Mapping Classes(类映射)
public class MyClass
{
[BsonDateTimeOptions(DateOnly = true)]
public DateTime DateOfBirth { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime AppointmentTime { get; set; }
}
在C#定义类时,对时间类型的属性使用BsonDateTimeOptions 特性进行标记,并指定 Kind = DateTimeKind.Local,此时当MyClass存储到Mongo时(序列化),本地时间会自动转为UTC时间,当从Mongo查询取出MyClass时(反序列化),UTC时间会自动转为本地时间。此时在写代码时是不需要对时间数据的时区进行考虑。
查询与聚合数据时的时区问题
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
并不能解决所有在使用Mongo时关于时区的问题。其中最重要的就是以时间为查询条件时按照时间进行分组统计时的问题。先看下简单查询时的时区问题。
使用 Builders Filter 进行查询![](https://img-blog.csdnimg.cn/bd54c2298de14ad58a8dcadb52ee2e18.png)
在使用BuilderFilter进行查询时,local时间和unspc时间会被自动转换为对应的utc时间进行查询。也就是说,时间类型进行查询比较时,在Mongo内无论是数据还是用来比较的时间都是按照UTC时间来的。
使用Linq进行查询
使用BsonDocument进行查询
直接使用DateTime类型
使用BsonDateTime类型
简单查询时,不需要特别关注用于比较的时间变量的时区问题,库会自动进行时区转换,保证local可以正确转成对应的UTC时间。
按时间分组时的时区问题
使用Linq进行聚合
var res = ContinuousDatas.Aggregate()
.Group(
c => new DateTime
(c.TimeStamp.Year, c.TimeStamp.Month, c.TimeStamp.Day, 0, 0, 0),
s => new
{
Id = s.Key,
Result = s.Sum(a => (double)a.Value)
}
);
对数据按天分组后进行求和的聚合操作,此时就会存在时区问题。
首先我们看下这个Linq会被Driver自动转为什么样的Linq
[{
"$group": {
"_id": {
"$dateFromParts": {
"year": {
"$year": "$TimeStamp"
},
"month": {
"$month": "$TimeStamp"
},
"day": {
"$dayOfMonth": "$TimeStamp"
},
"hour": 0,
"minute": 0,
"second": 0
}
},
"Result": {
"$sum": "$Value"
}
}
}]
可以看到,Linq被转换成了$dateFromParts功能,因为Mongo内部存储时间均为UTC时间,所以DateFromParts函数的结果其实是将UTC时间下相同的年月日会被归为一组,但是我们本来的目的是要在Local时区下相同的年月日分为一组。造成的结果就是:
数据库中,按Local时区的话,上图中三项数据都应该属于7月9号。但分组结果如下:
查阅Mongo官方文档可知,
$dayOfMonth (aggregation) — MongoDB Manual
获取时间的天数功能是有timezone 这个时区参数的,也就是说从Linq转换到Mongo查询语句时,如果能正确带入时区参数,问题就会得到解决。经过测试,当前版本(2.16.1)的C# Driver无法传入时区参数。所以目前如果存在按时间分组的聚合需求,仍然建议按照下面的示例使用BsonDocument进行聚合操作。
使用BsonDocument进行聚合
group.Add("_id",new BsonDocument().Add("$dateToString", new BsonDocument()
.Add("format", "%Y-%m-%d %H:00:00")
.Add("date", $"${nameof(ContinuousData.TimeStamp)}")
.Add("timezone", "+08")
));
如上,通过添加timezone参数,可使Mongo执行dateToString(其他类似)操作时按时区转换时间,此时则可以按时间正确分组。正确计算了,7月9号的三条数据之和。
总结
总的来说,在进行简单的查询时,无论使用何种查询方式,在设置了,[BsonDateTimeOptions(Kind = DateTimeKind.Local)],以及正确设置传入时间的时区后,是不需要再关注时区问题的。
再按时间进行分组聚合时,则需要考虑时区问题。此处建议在MongoC#Driver支持linq的时候传入timezone参数前,此种需求使用BsonDocument构建聚合语句来完成,并注意timezone参数。