改めてFirestoreのドキュメント更新について調べてみて、そしてESLintのカスタムルールを作りました!

# はじめに

Firestoreのドキュメントを更新するには、`updateDoc`, `setDoc`の2種類がありますが、2種類あるだけに用法が違います。

結構注意しないと危ないので、実際に動かして挙動を確認しながら使い方を見ていきたいと思います。

あと、注意点に関してはESLintのカスタムルールを作ったので、それで防げることも確認しようと思います。

# updateDoc

v8であれば、DocumentReference.updateですね。

この関数は純粋に更新を目的とした関数です。
よって、存在するドキュメントでのみ更新が可能で、引数に指定したオブジェクトのキーのフィールドを上書き更新する操作が行えます。
存在しないドキュメントでこの関数を使った場合は例外が発生します。

```
Uncaught (in promise) FirebaseError: No document to update
```

updateDocでは、「ドット表記」を使用することでネストしたデータを追加することができます。

```js
updateDoc(
  doc(getFirestore(), "messages", "すでにあるID"),
  { 'content.message': 'test' },
);
```

この結果、データはjson記法で書くと以下の形になります。
```json
{
  "content": {
    "message": "test"
  }
}
```

この「ドット表記」を使えば、事前にあるオブジェクトを壊さずにデータを追加できます。
たとえば、すでにデータが以下のような場合は、

```json
{
  "content": {
    "message": "aaaa"
    "createdAt": "2023/01/22 10:10:10"
  }
}
```

messageの部分のみが更新されます。
逆に「ドット表記」を使わない場合はどうなるのでしょう。

```js
updateDoc(
  doc(getFirestore(), "messages", "すでにあるID"),
  { content: { message: 'test' }},
);
```
このように記載した場合は、以下のようにcontentのところが置き換わってしまいます。

```json
{
  "content": {
    "message": "test"
  }
}
```

updateDocはこれくらい押さえておくと大丈夫だと思います。
そして、個人的には更新することが目的であれば、こちらを採用しています。

さらに注意点です。
「ドット表記」を使えば、ネストしていた場合も更新できるのですが、ドット表示を使う場合は、キーの中に"/", "*", "[", "]", "~"が利用できません。
これらの文字を利用したい場合は、次に記載する`setDoc`のマージを利用しましょう。

# setDoc

v8であれば、DocumentReference.setです。

こちらは、更新だけでなく追加もできます。
なので、ドキュメントのidが同じものであれば更新、なければ追加が実行されます。

ただ、大事なのはこれからで`updateDoc`と違う動作になります。

単純に`updateDoc`と同じように書くと、`setDoc`は更新というより、ドキュメントをすべて上書きします。
例えば、以下の状態のドキュメントがあるとして、`setDoc`を実行してみます。

```json
{
  "content": {
    "message": "aaaa"
    "createdAt": "2023/01/22 10:10:10"
  }
}
```

```js
setDoc(
  doc(getFirestore(), "messages", "すでにあるID"),
  { content: {key: 'test'} },
);
```

このsetDocを実行すると以下の結果になります。
```json
{
  "content": { "key": "test"}
}
```

完全に新しいものになってしまいました。
これはもはや更新とは言わないですね・・・。
ただこれは、第三引数にオプションを追加することで更新の動きになります。
さきほどの関数を書き換えてみます。
```js
setDoc(
  doc(getFirestore(), "messages", "すでにあるID"),
  { content: {key: 'test'} },
  { merge: true }
);
```

これを実行すると、以下の形になります。
```json
{
  "content": {
    "key": "test"
    "message": "aaaa"
    "createdAt": "2023/01/22 10:10:10"
  }
}
```

すでに存在するオブジェクトの中に追加されました！
これは`updateDoc`ではできません。
`updateDoc`で同じようにオブジェクトの形で指定するとcontent自体が上書きされてしまいます。
オプションの名前の通り、`setDoc`はオブジェクトをマージするような更新が行えます。
このような動作になるためなのか、`setDoc`で「ドット表記」を使うと、ドット表記がそのままキーとなって追加されてしまうので、注意が必要です。

また`setDoc`は`mergeFields`というキーを使うことができます。
これは、更新する場所を特定するオプションで、指定したキーのみが更新されます。
例えば、以下のように書くと、contentのkeyのところだけが更新されます。

```js
setDoc(
  doc(getFirestore(), "messages", "すでにあるID"),
  { content: {key: 'test', other: 'aaaa'} },
  { mergeFields: ['content.key'] }
);
```

なかなか使うことがなさそうなオプションです。
ただ、仕様によってはオブジェクトの中の一部だけを書き換えたいときがあるかもしれないので、このオプションが有効に使えるかもしれません。

ここまで、説明してきましたが、よっぽどじゃない限り、`setDoc`を更新で使う必要がないと思われます。

`setDoc`が有効になるケースは、追加もしくは更新を同時に満たしたい時、以前のデータをなかったことにして上書きしたいときです。

ただ、前者である場合は、更新をしたいわけなので、`merge: true`を忘れると危険なことになります！

さらにこれもかなり特殊なケースですが、すでにあるフィールドがMAP型になっていて、「ドット表記」では表現できないときにも`setDoc`を使う必要があります。
例えば以下のようにMAPのキーに"/"などが入るケースです。

```ts
setDoc(
  doc(getFirestore(), "messages", "すでにあるID"),
  {
    content: {
      key: 'test',
      ['1/2']: 'aaaa'
    }
  },
  { merge: true }
);
```

遭遇するケースはあまりなさそうですが、キーをユーザーが編集できるような形にしているときなどがこのケースに当たる可能性があります。

# ESLintでsetDocの危険を察知する

setDocを使いたい時、使ってしまった時であっても、`merge: true`をつけたくないというときはほとんどないと思います。
そこでESLintで警告できるようにしましょう！

https://www.npmjs.com/package/@sonicgarden/eslint-plugin-firestore

私が作ったものです!

`merge`もしくは`mergeFields`を`setDoc`に入れずに使ってた場合に警告を出すことができます。
修正も提案するようにしていて、`merge: true`を追加するかupdateDocに変更するかを提案してくれます。
Admin SDKでも利用できるように`doc().set()`の形でも動くようにしています!

実際のVSCodeで動かしたのが以下の画像です。

<img width="400" alt="vscode_preview" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341912/6800bebf-bd91-f4c9-d659-5d79d6f4761f.png" />

これで間違って全データを上書きしてしまうことはなくなるはず！

# 終わりに

ほとんどは、公式ドキュメントに書いていますが、`mergeFields`なんかは書かれていないので私も最近知った機能です。
Firestoreの基本的なところですが、深く触ってると必要になる知識だと思います！

また、`setDoc`の用途は、私が書いた以上にあるのかが気になっているので、こんなときは`setDoc`を使った方がいいよ！というのがあれば、ぜひコメントしていただけると助かります！

あとはESLint使ってもらえるとうれしいです！不具合があればissueも出していただければ対応します！