介绍

日期和时间数据通常由数据库系统管理,而且非常重要,但正确处理起来往往比最初看起来更棘手。数据库必须能够以清晰、明确的格式存储日期和时间数据,将这些数据转换为用户友好的格式,以便与客户应用程序进行交互,并在考虑到不同时区和夏令时变化等复杂因素的情况下执行基于时间的操作。

MongoDB日期和时间类型

MongoDB中的DATE类型可以将日期和时间值作为一个组合单位来存储。
这里,左边一列代表数据类型的BSON(二进制JSON)名称,第二列代表与该类型相关的ID号。最后的 "别名 "列表示MongoDB用来表示该类型的字符串:

Type          | Number |     Alias    |
  ------------------ | ------ | ------------ |
        Date         |    9   |     "date"   |

BSON Date类型是一个有符号的64位整数,代表自Unix诞生(1970年1月1日)以来的毫秒数。正数代表自纪元以来所经过的时间,而负数代表从纪元向后移动的时间。
将日期和时间数据存储为一个大的整数是有益的,因为它:

  • 允许MongoDB以毫秒级的精度存储日期
  • 在如何显示日期和时间方面提供了灵活性

因为日期类型不存储额外的信息,如时区,如果相关的话,必须单独存储该背景。MongoDB将在内部使用UTC来存储日期和时间信息,但在检索时可以根据需要轻松转换为其他时区。
MongoDB还提供了一个主要用于内部的Timestamp类型:

Type         | Number |     Alias    |
  ------------------| ------ | ------------ |
      Timestamp     |   17   |  "timestamp" |

这主要是为了协助复制和分片等内部过程而实现的,不应该在应用程序的逻辑中使用这个。日期类型通常可以满足可能有的对时间的任何要求。

如何创建新日期

可以通过两种不同的方式创建一个新的Date对象:

  • new Date():将一个日期和时间作为一个Date对象返回。
  • ISODate():将一个日期和时间作为一个Date对象返回。

new Date()和ISODate()方法都会产生一个被ISODate()辅助函数包裹的Date对象。
此外,调用Date()函数而不使用新的构造函数,会返回一个字符串形式的日期和时间,而不是一个日期对象:
Date():返回一个日期和时间的字符串。
了解这两种类型之间的区别是很重要的,因为它影响到哪些操作是可用的,信息的存储方式及灵活性。一般来说,最好使用Date类型来存储日期信息,然后根据需要对其进行格式化输出。
可以看下MongoDB shell会话中的工作过程:
首先,切换到一个新的临时数据库,并创建三个各有一个日期字段的文档。使用不同的方法来填充每个对象的日期字段:

use temp_db

db.dates.insertMany([
    {
        name: "Created with `Date()`",
        date: Date(),
    },
    {
        name: "Created with `new Date()`",
        date: new Date(),
    },
    {
        name: "Created with `ISODate()`",
        date: ISODate(),
    },
])
{
    "acknowledged" : true,
    "insertedIds" : [
        ObjectId("62726af5a3dc7398b97e6e93"),
        ObjectId("62726af5a3dc7398b97e6e94"),
        ObjectId("62726af5a3dc7398b97e6e95")
    ]
}

默认情况下,这些机制中的每一个都会存储当前的日期和时间。可以通过添加一个ISO 8601格式的日期字符串作为参数来存储一个不同的日期和时间:

db.dates.insertMany([
    {
        name: "Future date",
        date: ISODate("2040-10-28T23:58:18Z"),
    },
    {
        name: "Past date",
        date: new Date("1852-01-15T11:25"),
    },
])

这些将在适当的日期和时间创建一个Date对象。
有一点需要注意的是,在上述第一个新文件中加入了尾部的Z。这表明日期和时间被提供为UTC。指定不带Z的日期将导致MongoDB根据当前的本地时间解释输入(尽管它总是在内部将其转换并存储为UTC日期)。
验证日期对象的类型
然后,可以显示所产生的文件,看看MongoDB是如何存储日期数据的:

db.dates.find().pretty()

{
    "_id" : ObjectId("62726af5a3dc7398b97e6e93"),
    "name" : "Created with `Date()`",
    "date" : "Wed May 04 2022 12:00:53 GMT+0000 (UTC)"
}
{
    "_id" : ObjectId("62726af5a3dc7398b97e6e94"),
    "name" : "Created with `new Date()`",
    "date" : ISODate("2022-05-04T12:00:53.307Z")
}
{
    "_id" : ObjectId("62726af5a3dc7398b97e6e95"),
    "name" : "Created with `ISODate()`",
    "date" : ISODate("2022-05-04T12:00:53.307Z")
}
{
    "_id" : ObjectId("62728b57a3dc7398b97e6e96"),
    "name" : "Future date",
    "date" : ISODate("2040-10-28T23:58:18Z")
}
{
    "_id" : ObjectId("62728c5ca3dc7398b97e6e97"),
    "name" : "Past date",
    "date" : ISODate("1852-01-15T11:25:00Z")
}

正如预期的那样,用ISODate()和new Date()的日期字段包含Date对象(被ISODate帮助器包装)。相反,由Date()函数调用所填充的字段被存储为一个字符串。
可以通过在集合上调用一个map函数来验证哪些日期字段包含一个实际的Date对象。该函数检查每个日期字段,看它存储的对象是否是Date类型的实例,并在一个名为is_a_Date_object的新字段中显示结果。此外,可以使用valueOf()方法来显示每个日期字段是如何被MongoDB实际存储的:

db.dates.find().map(
    function(date_doc) {
        date_doc["is_a_Date_object"] = date_doc.date instanceof Date;
        date_doc["date_storage_value"] = date_doc.date.valueOf();
        return date_doc;
    }
)
[
    {
        "_id" : ObjectId("62726af5a3dc7398b97e6e93"),
        "name" : "Created with `Date()`",
        "date" : "Wed May 04 2022 12:00:53 GMT+0000 (UTC)",
        "is_a_Date_object" : false,
        "date_storage_value" : "Wed May 04 2022 12:00:53 GMT+0000 (UTC)"
    },
    {
        "_id" : ObjectId("62726af5a3dc7398b97e6e94"),
        "name" : "Created with `new Date()`",
        "date" : ISODate("2022-05-04T12:00:53.307Z"),
        "is_a_Date_object" : true,
        "date_storage_value" : 1651665653307
    },
    {
        "_id" : ObjectId("62726af5a3dc7398b97e6e95"),
        "name" : "Created with `ISODate()`",
        "date" : ISODate("2022-05-04T12:00:53.307Z"),
        "is_a_Date_object" : true,
        "date_storage_value" : 1651665653307
    },
    {
        "_id" : ObjectId("62728b57a3dc7398b97e6e96"),
        "name" : "Future date",
        "date" : ISODate("2040-10-28T23:58:18Z"),
        "is_a_Date_object" : true,
        "date_storage_value" : 2235081498000
    },
    {
        "_id" : ObjectId("62728c5ca3dc7398b97e6e97"),
        "name" : "Past date",
        "date" : ISODate("1852-01-15T11:25:00Z"),
        "is_a_Date_object" : true,
        "date_storage_value" : -3722502900000
    }
]

这也证实了显示为ISODATE(…)的字段是Date类型的实例,而用Date()函数创建的日期则不是。
此外,上面的输出显示,以Date类型存储的对象被记录为有符号的整数。正如预期的那样,与1852年的日期相关的日期对象是负数,因为它是从1970年1月开始倒数的。

日期对象的查询

如果有一个日期混合表示的集合,可以使用$type操作符查询有匹配类型的字段。
例如,要查询所有日期为Date对象的文档,可以输入:

db.dates.find({
    date: { $type: "string" },
}).pretty()

{
    "_id" : ObjectId("62726af5a3dc7398b97e6e93"),
    "name" : "Created with `Date()`",
    "date" : "Wed May 04 2022 12:00:53 GMT+0000 (UTC)"
}

日期类型允许执行获取时间单位之间关系的查询。
例如,可以按顺序比较Date对象。如果要检查未来的日期,可以键入:

db.dates.find({
    date: {
        $gt: new Date()
    }
}).pretty()
{
    "_id" : ObjectId("62728b57a3dc7398b97e6e96"),
    "name" : "Future date",
    "date" : ISODate("2040-10-28T23:58:18Z")
}

如何使用Date类型的方法

可以用各种包含的方法和运算符对Date对象进行操作。例如,可以从一个日期中提取不同的日期和时间成分,并以许多不同的格式打印。
下面做个演示:
首先,从一个带有日期对象的文档中选择日期:

date_obj = db.dates.findOne({"name": "Future date"}).date

然后,选择日期字段,并通过调用对象上的各种方法从其中提取不同的组件:

date_obj.getUTCFullYear()
date_obj.getUTCMonth()
date_obj.getUTCDate()
date_obj.getUTCHours()
date_obj.getUTCMinutes()
date_obj.getUTCSeconds()
2040    // year
9       // month
28      // date
23      // hour
58      // minutes
18      // seconds

还有一些配套的方法,可以通过提供不同的时间和日期组件来设置时间。例如,通过调用.setUTCFullYear()方法改变年份:

date_obj.toString()
date_obj.setUTCFullYear(2028)
date_obj.toString()
date_obj.setUTCFullYear(2040)
Sun Oct 28 2040 23:58:18 GMT+0000 (UTC)
1856390298000  // integer stored for the new date value
Sat Oct 28 2028 23:58:18 GMT+0000 (UTC)
2235081498000  // integer stored for the restored date value

还可以把日期输出不同的格式来显示:

date_obj.toDateString()
date_obj.toUTCString()
date_obj.toISOString()
date_obj.toLocaleDateString()
date_obj.toLocaleTimeString()
date_obj.toString()
date_obj.toTimeString()
Sun Oct 28 2040                          // .toDateString()
Sun, 28 Oct 2040 23:58:18 GMT            // .toUTCString()
2040-10-28T23:58:18.000Z                 // .toISOString()
10/28/2040                               // .toLocaleDateString()
23:58:18                                 // .toLocaleTimeString()
Sun Oct 28 2040 23:58:18 GMT+0000 (UTC)  // .toString()
23:58:18 GMT+0000 (UTC)                  // .toTimeString()

这些都是与JavaScript的Date类型相关的主要方法。

如何使用MongoDB日期聚合函数

MongoDB还提供了其他一些可以操作日期的函数。其中一个有用的例子是java mongodb 时间查询 mongodb查询日期_字段dateToString()来传递一个Date对象、一个格式字符串和一个时区参数。MongoDB将使用格式字符串作为模板来计算如何输出给定的Date对象,并使用时区来正确偏移输出的UTC。
在这里,演示使用一个字符串格式化日期集合中的日期,并把这些日期转换成纽约时区。
首先,需要删除任何可能将日期字段保存为字符串的文档:
db.dates.deleteMany({“date”: {KaTeX parse error: Expected 'EOF', got '}' at position 15: type: "string"}̲}) 然后,可以用dateToString函数运行一个聚合:

db.dates.aggregate(
    [
        {
            $project: {
                "_id": 0,
                "date": "$date",
                "my_date": {
                    $dateToString: {
                        date: "$date",
                        format: "Day %d of Month %m (Day %j of year %Y) at %H hours, %M minutes, and %S seconds (timezone offset: %z)",
                        timezone: "America/New_York",
                    }
                }
            }
        }
    ]
).pretty()
{
    "date" : ISODate("2022-05-04T12:00:53.307Z"),
    "my_date" : "Day 04 of Month 05 (Day 124 of year 2022) at 08 hours, 00 minutes, and 53 seconds (timezone offset: -0400)"
}
{
    "date" : ISODate("2022-05-04T12:00:53.307Z"),
    "my_date" : "Day 04 of Month 05 (Day 124 of year 2022) at 08 hours, 00 minutes, and 53 seconds (timezone offset: -0400)"
}
{
    "date" : ISODate("2040-10-28T23:58:18Z"),
    "my_date" : "Day 28 of Month 10 (Day 302 of year 2040) at 19 hours, 58 minutes, and 18 seconds (timezone offset: -0400)"
}
{
    "date" : ISODate("1852-01-15T11:25:00Z"),
    "my_date" : "Day 15 of Month 01 (Day 015 of year 1852) at 06 hours, 28 minutes, and 58 seconds (timezone offset: -0456)"
}

$dateToParts()函数可以用来将一个Date字段分解成它的组成部分。
例如,可以输入:

db.dates.aggregate(
    [
        {
            $project: {
                _id: 0,
                date: {
                    $dateToParts: { date: "$date" }
                }
            }
        }
    ]
)
{ "date" : { "year" : 2022, "month" : 5, "day" : 4, "hour" : 12, "minute" : 0, "second" : 53, "millisecond" : 307 } }
{ "date" : { "year" : 2022, "month" : 5, "day" : 4, "hour" : 12, "minute" : 0, "second" : 53, "millisecond" : 307 } }
{ "date" : { "year" : 2040, "month" : 10, "day" : 28, "hour" : 23, "minute" : 58, "second" : 18, "millisecond" : 0 } }
{ "date" : { "year" : 1852, "month" : 1, "day" : 15, "hour" : 11, "minute" : 25, "second" : 0, "millisecond" : 0 } }

MongoDB关于聚合函数的文档有关于其他函数的信息,可以用来操作Date对象进行显示或比较。

总结

在本文中,介绍了一些可以在MongoDB中处理日期和时间数据的不同方式。大多数时间性数据可能应该存储在MongoDB的Date数据类型中,因为这在操作数据或显示数据时提供了很大的灵活性。
熟悉日期和时间数据是如何在内部存储的,如何在输出时将其胁迫成理想的格式,以及如何比较、修改和将数据分解成有用的块状,可以解决许多不同的问题。虽然日期信息在工作中可能具有挑战性,但利用现有的方法和操作符可以帮助减轻一些繁重的工作。