分享至

介绍

日期和时间数据通常由数据库系统管理,非常重要,但往往比最初看起来更难正确处理。数据库必须能够以清晰、明确的格式存储日期和时间数据,将该数据转换为用户友好的格式以与客户端应用程序交互,并执行考虑时区和夏令时变化等复杂因素的时间操作。

在本指南中,我们将讨论 MongoDB 提供的一些用于有效处理日期和时间数据的工具。我们将探索相关数据类型,查看运算符和方法,并介绍如何最好地使用这些工具来保持日期和时间数据的良好秩序。

MongoDB DateTimestamp 类型

DATE 类型在 MongoDB 中 可以将日期和时间值存储为一个组合单元。

这里,左列表示 BSON(二进制 JSON) 数据类型的名称,第二列表示与该类型关联的 ID 号。最后的“别名”列表示 MongoDB 用于表示该类型的字符串

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

BSON 日期类型是一个带符号的 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 对象。

此外,在没有 new 构造函数的情况下调用 Date() 函数会将日期和时间作为字符串返回,而不是 Date 对象

  • Date():将日期和时间作为字符串返回。

重要的是要注意这两种类型之间的区别,因为它会影响可用的操作、信息的存储方式以及它将赋予您的灵活性。一般来说,几乎总是最好使用 Date 类型存储日期信息,然后根据需要将其格式化为输出。

让我们看看这在 MongoDB shell 会话中是如何工作的。

首先,我们可以切换到一个新的临时数据库并创建三个文档,每个文档都有一个 date 字段。我们对每个对象的 date 字段使用不同的填充方法

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 日期并在内部存储为 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 字段包含 Date 对象(包装在 ISODate 帮助器中)。相反,用裸 Date() 函数调用填充的字段存储为字符串。

您可以通过对集合调用 map 函数来验证哪些 date 字段包含实际的 Date 对象。映射检查每个 date 字段以查看它存储的对象是否是 Date 类型的实例,并在名为 is_a_Date_object 的新字段中显示结果。此外,我们将使用 valueOf() 方法来显示每个 date 字段实际上是如何由 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 不是。

此外,以上输出显示用 Date 类型存储的对象被记录为带符号整数。正如预期的那样,与 1852 年的日期相关的日期对象是负的,因为它从 1970 年 1 月开始倒数。

查询日期对象

如果您有一个包含日期混合表示的集合,您可以使用 $type 运算符查询具有匹配类型的字段。

例如,要查询所有 dateDate 对象的文档,您可以键入

db.dates
.find({
date: { $type: 'date' },
})
.pretty()
{
"_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")
}

要查找 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 类型允许您执行理解时间单位之间关系的查询。

例如,您可以按顺序比较 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 字段,并通过在对象上调用各种方法来提取其中的不同组件

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 Date 聚合函数

MongoDB 提供了一些其他函数,也可以用来操作日期。一个有用的例子是 $dateToString() 聚合函数。您可以使用 Date 对象、格式字符串说明符和时区指示符来传递调用 $dateToString()。MongoDB 将使用格式字符串作为模板来确定如何输出给定的 Date 对象,并将使用时区来正确地从 UTC 偏移输出。

在这里,我们将使用任意字符串来格式化我们 dates 集合中的日期。我们还将日期转换为纽约时区。

首先,我们需要删除可能将 date 字段保存为字符串的任何流浪文档

db.dates.deleteMany({ date: { $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 } }

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

结论

在本指南中,我们介绍了一些在 MongoDB 中使用日期和时间数据的方法。大多数时间数据可能应该存储在 MongoDB 的 Date 数据类型中,因为这在操作数据或显示数据时提供了很大的灵活性。

熟悉日期和时间数据是如何在内部存储的、如何强制它在输出时转换为所需的格式,以及如何比较、修改和将数据分解成有用的块,可以帮助您解决许多不同的问题。虽然日期信息可能难以处理,但利用可用的方法和运算符可以帮助减轻一些繁重的工作。

关于作者
Justin Ellingwood

Justin Ellingwood

Justin 自 2013 年以来一直在撰写有关数据库、Linux、基础设施和开发人员工具的文章。他目前与妻子和两只兔子住在柏林。他通常不必以第三人称写作,这对所有相关方来说都是一种解脱。