MongoDB 配列のフィールドを更新するarrayFiltersについて

# はじめに
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です。

:::note
しれっと書いていますが、同じフィールドに対して別々の識別子を割り当てるのは可能です。
上記だと`test`と`test2`は同じ`tests`フィールドを参照しています。
:::

# 多段ネストされた配列
ちなみにですが、<INS>配列の中に配列のフィールドがある</INS>場合でもこの`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 } }]  }
)
```

ここまで分かっていれば当然の書き方だと思いますが、ちゃんと対応してくれますね。
そもそもあんまりネストさせすぎると扱いにくいので、そこは考える必要がありますが…

# 参考リンク
- [$[<identifier>] - MongoDB Manual v7.0](https://www.mongodb.com/docs/manual/reference/operator/update/positional-filtered/)