共有

はじめに

日付と時刻のデータはデータベースシステムで一般的に管理されており、非常に重要ですが、最初は思えるよりも正しく扱うのが難しい場合があります。データベースは、日付と時刻のデータを明確で曖昧でない形式で保存し、そのデータをクライアントアプリケーションと対話するためにユーザーフレンドリーな形式に変換し、異なるタイムゾーンや夏時間の変更などの複雑さを考慮して時間ベースの操作を実行できる必要があります。

このガイドでは、MongoDBが日付と時刻のデータを効果的に操作するために提供するいくつかのツールについて説明します。関連するデータ型を探り、演算子とメソッドを確認し、これらのツールを最大限に活用して日付と時刻のデータを適切に管理する方法を詳しく説明します。

MongoDBのDate型とTimestamp

MongoDBのDATEは、日付と時刻の値を結合された単位として格納できます。

ここで、左の列はデータ型のBSON(バイナリJSON)名を表し、2番目の列はその型に関連付けられたID番号を表します。最後の「Alias」列は、MongoDBがその型を表すために使用する文字列を表します。

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

BSON Date型は、Unixエポック(1970年1月1日)からのミリ秒数を表す符号付き64ビット整数です。正の数はエポックからの経過時間を表し、負の数はエポックから遡る時間を表します。

日付と時刻のデータを大きな整数として保存することは、以下の理由から有益です。

  • MongoDBが日付をミリ秒単位で精度良く保存できる
  • 日付と時刻の表示方法に柔軟性を提供する

日付型はタイムゾーンのような追加情報を保存しないため、関連する場合はそのコンテキストを別途保存する必要があります。MongoDBは日付と時刻の情報を内部的にUTCを使用して保存しますが、必要に応じて取得時に他のタイムゾーンに簡単に変換できます。

MongoDBは、主に内部的に使用されるTimestampも提供しています。

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

これは主にレプリケーションやシャーディングなどの内部プロセスを調整するために実装されているため、独自のアプリケーションロジックでこれを使用すべきではありません。日付型は、通常、時刻に関するあらゆる要件を満たすことができます。

新しい日付を作成する方法

新しいDateオブジェクトを2つの異なる方法で作成できます。

  • new Date(): 日付と時刻をDateオブジェクトとして返します。
  • ISODate(): 日付と時刻をDateオブジェクトとして返します。

new Date()メソッドとISODate()メソッドは両方とも、ISODate()ヘルパー関数でラップされたDateオブジェクトを生成します。

さらに、newコンストラクタなしでDate()関数を呼び出すと、Dateオブジェクトではなく文字列として日付と時刻が返されます。

  • Date(): 日付と時刻を文字列として返します。

これら2つの型の違いを念頭に置くことが重要です。なぜなら、利用可能な操作、情報の保存方法、そしてどれだけの柔軟性が得られるかに影響するからです。一般的に、日付情報はDate型を使用して保存し、必要に応じて出力用にフォーマットするのがほとんど常に最善です。

MongoDBシェルセッションでこれがどのように機能するかを見てみましょう。

まず、新しい一時データベースに切り替え、それぞれdateフィールドを持つ3つのドキュメントを作成します。各オブジェクトの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オブジェクトを作成します。

注目すべき点の1つは、上記の最初の新しいドキュメントの末尾に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フィールドには、Dateオブジェクト(ISODateヘルパーにラップされています)が含まれています。対照的に、生のDate()関数呼び出しによって入力されたフィールドは文字列として保存されます。

コレクションに対してmap関数を呼び出すことで、どのdateフィールドが実際のDateオブジェクトを含んでいるかを確認できます。mapは各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は、日付を操作できる他の関数も提供しています。その有用な例の1つは、$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 } }

MongoDBの集計関数に関するドキュメントには、表示や比較のためにDateオブジェクトを操作するために使用できる追加関数に関する情報があります。

結論

このガイドでは、MongoDB内で日付と時刻のデータを操作するさまざまな方法について説明しました。ほとんどの時間データは、MongoDBのDateデータ型で保存するのがおそらく最適です。これにより、データの操作や表示においてかなりの柔軟性が得られます。

日付と時刻のデータが内部でどのように保存されるか、出力時に望ましい形式に強制変換する方法、そしてデータを比較、変更、有用なチャンクに分解する方法に慣れることで、さまざまな問題を解決できます。日付情報の扱いは難しい場合がありますが、利用可能なメソッドや演算子を活用することで、一部の重労働を軽減できます。

著者について
Justin Ellingwood

ジャスティン・エリングウッド

ジャスティンは2013年からデータベース、Linux、インフラストラクチャ、開発者ツールについて執筆しています。現在はベルリンで妻と2匹のウサギと暮らしています。通常、三人称で書く必要がないため、関係者全員にとって安心です。
© . All rights reserved.