はじめに
MongoDBのUPDATE系のオペレーション(updateOne、updateMany等)には、オプションとしてarrayFilter
が存在しています。
配列のフィールドに対して更新を行う際に、その配列の中での条件を設定できる便利なオプションですね。
(元々あまりネストさせすぎると扱いにくくなるという話は置いておく)
簡単な例としては、下記のような感じです。
# test_resultコレクションのデータ(_id省略)
[{
"no": 1,
"name": "John",
"tests": [
{ "subject": "math", "score": 71 },
{ "subject": "chemistry", "score": 63 },
{ "subject": "physics", "score": 55 },
{ "subject": "biology", "score": 87 }
]
}]
# tests内のsubject="physics"のsocreを3加算する
db.test_reslt.updateOne(
{ no: 1 },
{ $inc: { 'tests.$[test].score': 3 } },
{ arrayFilters: [{ 'test.subject': 'physics' }] }
)
# 結果
[{
"no": 1,
"name": "John",
"tests": [
{ "subject": "math", "score": 71 },
{ "subject": "chemistry", "score": 63 },
{ "subject": "physics", "score": 58 },
{ "subject": "biology", "score": 87 }
]
}]
このようにドキュメント内のtests
フィールドが配列となっていますが、その中の特定条件の要素を指定してUPDATEを実行することができます。
UPDATEのオペレータの中(今回だと$inc
)で$[{任意の識別子}]
を設定し、arrayFilters
でその識別子に対する条件を設定する形で使用します。(今回だと識別子は"test"としています)
やや面倒ですが、これを使えばネストされた配列の中を操作出来るので覚えておいて損はないでしょう。
細かい制約
さて、そんなarrayFilters
ですが細かい制約があります。
識別子には半角英数字しか使えず、先頭は半角英字のみ
たまに見るalphanumeric制約かつ先頭の文字の制限ですね。
つまり下記のクエリだと実行エラーになるので気をつけましょう。
# 半角英数字以外を含んでいる
db.test_reslt.updateOne(
{ no: 1 },
{ $inc: { 'tests.$[テスト].score': 3 } },
{ arrayFilters: [{ 'テスト.subject': 'physics' }] }
)
# 先頭が半角英字ではない
db.test_reslt.updateOne(
{ no: 1 },
{ $inc: { 'tests.$[Test].score': 3 } },
{ arrayFilters: [{ 'Test.subject': 'physics' }] }
)
同じ識別子は一つの要素にまとめなければならない
文章だとちょっと分かりにくいですが、要は下記のような制約です。
# test_resultコレクションのデータ(_id省略)
[{
"no": 1,
"name": "John",
"tests": [
{ "subject": "math", "score": 71 },
{ "subject": "chemistry", "score": 63 },
{ "subject": "physics", "score": 55 },
{ "subject": "biology", "score": 87 }
]
}]
# これはエラー
db.test_result.updateOne(
{ name: 'John' },
{ $inc: { 'tests.$[test].score': 5 } },
{ arrayFilters: [{ 'test.subject': 'chemistry' }, { 'test.score': { $gte: 60 } }] }
)
# これはOK
db.test_result.updateOne(
{ name: 'John' },
{ $inc: { 'tests.$[test].score': 5 } },
{ arrayFilters: [{ 'test.subject': 'chemistry', 'test.score': { $gte: 60 } }] }
)
arrayFilters
は配列なのですが、同じ識別子なら同じ要素のオブジェクト内に記述しないとダメだよ、ということです。
まあわざわざ分けて書く意味も無いので、これは大丈夫でしょう。
さらに言うと、arrayFilters
内で複数の識別子が同じ要素に入っているのもダメです。
# これはエラー
db.test_result.updateOne(
{ name: 'John' },
{ $inc: { 'tests.$[test].score': 5, 'tests.$[test2].score': 10 } },
{ arrayFilters: [{ 'test.subject': 'chemistry', 'test2.subject': 'biology' }] }
)
上記だと、識別子test
とtest2
が同じ要素内に入っているのでダメということですね。
ちゃんと識別子ごとに要素を分ける、と考えればOKです。
しれっと書いていますが、同じフィールドに対して別々の識別子を割り当てるのは可能です。
上記だとtest
とtest2
は同じtests
フィールドを参照しています。
多段ネストされた配列
ちなみにですが、配列の中に配列のフィールドがある場合でもこのarrayFilters
を使えば狙った要素を操作することが出来ます。
# classコレクションのデータ(_id省略)
{
"class": "red",
"members": [
{
"name": "John",
"location": "left",
"leader": false,
"results": [
{ "kind": "power", "retry": 0, "score": 50 },
{ "kind": "technic", "retry": 0, "score": 70 },
{ "kind": "speed", "retry": 1, "score": 40 }
]
},
{
"name": "Mary",
"location": "left",
"leader": true,
"results": [
{ "kind": "power", "retry": 0, "score": 40 },
{ "kind": "technic", "retry": 2, "score": 60 },
{ "kind": "speed", "retry": 1, "score": 80 }
]
},
{
"name": "Ann",
"location": "right",
"leader": true,
"results": [
{ "kind": "power", "retry": 2, "score": 90 },
{ "kind": "technic", "retry": 0, "score": 50 },
{ "kind": "speed", "retry": 0, "score": 70 }
]
}
]
}
# memberのlocation="left"かつresultのretryが1以上あるなら、そのresultのscoreを5減算する
db.class.updateOne(
{ class: 'red' },
{ $inc: { 'members.$[member].results.$[result].score': -5 } },
{ arrayFilters: [{ 'member.location': 'left' }, { 'result.retry': { $gte: 1 } }] }
)
ここまで分かっていれば当然の書き方だと思いますが、ちゃんと対応してくれますね。
そもそもあんまりネストさせすぎると扱いにくいので、そこは考える必要がありますが…