[MongoDB] ドキュメント内の配列をソートする
はじめに
MongoDBはひとつのドキュメント(RDBでいうところのレコード)にサブドキュメントや配列をネストして保持することが可能です。そのため、「ひとつのドキュメント内に含まれる配列を特定のキーでソートしたい」というニーズがあり、少々手こずったので方法を記載します。
具体的には以下のようなドキュメントを想定します。Aliceが投稿した本のレビューをreviewsとして配列で保持していますが、このドキュメントをstarやtitleでソートした状態で取得するにはどうすればよいか?という内容になります。
{
"_id": "62b448342a2920dbf51973d0",
"name": "Alice",
"reviews": [
{
"_id": "62b448342a2920dbf51973d1",
"title": "Perfect JavaScript",
"star": 3
},
{
"_id": "62b448342a2920dbf51973d2",
"title": "Readable Code",
"star": 5
},
{
"_id": "62b448342a2920dbf51973d3",
"title": "A Philosophy of Software Design",
"star": 5
},
{
"_id": "62b448342a2920dbf51973d4",
"title": "Code Complete",
"star": 4
}
],
}
結論
aggregation
を利用します。評価したい配列を$unwind
したうえで$sort
します。-1
は降順(DESC)を意味しており、以下のクエリはスターの高い順にソートします。1
なら昇順(ASC)になります。
const sorted = await UserModel.aggregate([
{
$match: {
_id: "62b448342a2920dbf51973d0"
}
},
{
$unwind: "$reviews"
},
{
$sort: {
"reviews.star": -1
}
},
{
$group: {
_id: "$_id",
name: {
$first: "$name"
},
reviews: {
$push: "$reviews"
}
}
}
]);
結果は以下のようになります。
{
"_id": "62b448342a2920dbf51973d0",
"name": "Alice",
"reviews": [
{
"_id": "62b448342a2920dbf51973d2",
"star": 5,
"title": "Readable Code"
},
{
"_id": "62b448342a2920dbf51973d3",
"star": 5,
"title": "A Philosophy of Software Design"
},
{
"_id": "62b448342a2920dbf51973d4",
"star": 4,
"title": "Code Complete"
},
{
"_id": "62b448342a2920dbf51973d1",
"star": 3,
"title": "Perfect JavaScript"
}
]
}
内部的な動作としては取得したドキュメントを$unwind
で一旦バラして、指定のキーでソートしたうえで$group
でまとめ直しているのかなと想像していますが、このあたりはあまり理解できていません。。。
通常のソート
通常、複数のドキュメントをソートするのであればsort
によるクエリが一般的で、これはSQLにおけるORDER BY
と同様に利用することが可能です。以下のようなコレクションから、ドキュメントをソートして取り出したい、というケースです。
{
"_id": "62b448342a2920dbf51973d2",
"star": 5,
"title": "Readable Code"
},
{
"_id": "62b448342a2920dbf51973d3",
"star": 5,
"title": "A Philosophy of Software Design"
},
{
"_id": "62b448342a2920dbf51973d4",
"star": 4,
"title": "Code Complete"
},
{
"_id": "62b448342a2920dbf51973d1",
"star": 3,
"title": "Perfect JavaScript"
}
const sorted = await Reviews.find().sort({"title":1});
非常に簡潔ですが、このsort
は「ドキュメント」が対象になるので、findOne
などドキュメントがひとつに絞られるケースでは利用できませんし(エラーは起きませんがソートされない)、ドキュメント内のネストした配列を対象にソートすることはできません。
ソートする対象がドキュメントなのか、ドキュメント内にネストしたオブジェクトなのか、によってソートの方法が異なるので注意が必要です。
配列の要素をフィルターしてからソートする
ソートに加えてフィルターする必要がでてきたので、そちらの方法も記載しておきます。
const filteredAndSorted = await UserModel.aggregate([
{
$match: {
_id: "62b448342a2920dbf51973d0"
}
},
{
$project: {
name: "$name",
reviews: {
$filter: {
input: "$reviews",
as: "review",
cond: {
$eq: [
"$$review.star",
5
]
}
}
}
}
},
{
$unwind: "$reviews"
},
{
$sort: {
"reviews.title": 1
}
},
{
$group: {
_id: "$_id",
name: {
$first: "$name"
},
reviews: {
$push: "$reviews"
}
}
}
]);
start
が5のレビューをフィルタしてタイトルの昇順で取得してみました。結果は以下のようになります。
{
"_id": "62b448342a2920dbf51973d0",
"name": "Alice",
"reviews": [
{
"_id": "62b448342a2920dbf51973d3",
"star": 5,
"title": "A Philosophy of Software Design"
},
{
"_id": "62b448342a2920dbf51973d2",
"star": 5,
"title": "Readable Code"
}
]
}
Discussion