Firebase CloudFunctionsでRealtimeDBからFirestoreに切り替えていく時の勘所

[Firebase Advent Calendar 2017](https://qiita.com/advent-calendar/2017/firebase)の12日目の記事です。

最近携わっているプロジェクトにて、DatabaseをRealtimeDBからFirestoreに移行し、併せてCloudFunctionsでの処理も移行したのでその時に得られた知見を、before/after踏まえて共有しようと思います

#### 事前に
基本的にはTypeScriptで書いていて、以下2つをimportしている事を前提としています。
JavaScriptの場合は適宜置き換えてお読み下さい。

```ts
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
```

また、Promiseではなくてasync/awaitを使っている点もご留意下さい。

## `database` -> `firestore`
DBのあるpathに対する変更をトリガーとしてfunctionを定義する場合は、`database`->`firestore`(と使う関数を少し)変更してあげるだけで済みます。`onCreate`といったイベント等はそのままです。

```ts:before
export const userCreated = functions.database.ref('/v1/user/{userID}').onCreate(async event => {
  // ...
})
```

```ts:after
export const userCreated = functions.firestore.document('/version/1/user/{userID}').onCreate(async event => {
  // ...
})
```

ただ、Firestoreの場合注意点があり、`document`で指定するpathの階層は偶数個でないとだめです。
これはFirestoreの特性でもありますが、 **(Sub)Collection->Document** の順でpathが構成されるため、`firestore.document()`の中で、beforeと同様に`/v1/user/{userID}`としてしまうと機能しなくなります。
これ、 *deploy時に気付かずうっかりやってしまうのでは...* **いえ、そんなことはありません。** `firebase deploy` コマンド実行時に、このpathがおかしかったりすると400エラーが返却され、deployに失敗するので、うっかり気づかずデプロイしてしまった！ということは起きません。

参考: [Cloud FirestoreのSubCollectionとQueryっていつ使うの問題](https://qiita.com/1amageek/items/d2ef7a49bccf5b4ea78e)

## 指定したpathのドキュメントを取得したい
指定したpathのデータ(ドキュメント)を取得したい場合は、以下のようになります

```ts:before
const userID = 'xxx'
const user = await admin.database().ref(`/v1/user/${userID}`).once('value').then(s => s.val())
console.log(user)
```

```ts:after
const userID = 'xxx'
const user = await admin.firestore().doc(`/version/1/user/${userID}`).get().then(s => s.data())
console.log(user)
```

一部が置き換わる、pathの扱いに気をつけるだけで、ほぼ変わりません。


## ネストしたデータを書き込む
例えば、taskというモデルに、何かしらエラーが発生した場合に、errorというkeyにたいして、エラーコードと詳細を書き込むことを想定してみます。


RealtimeDBでネストしたデータを保つ場合だと、taskのrefから`error`というchildRefを作り、そこにsetする形になります。

```ts:before
taskRef.child('error').set({
  code: 100,
  description: 'The operation could not be completed.'
})
```
taskRefが`v1/task/{taskID}` を表していたとすると、今回の追加で、 `v1/task/{taskID}/error` のpath(ref)が増えたことになります。


対してFirestoreは、DocumentのFieldにネストしたデータを置く場合は、ネストしたObject(json)の形式でsetしてあげるだけです。
RealtimeDBと違って、ネストした分がpathになってしまうこともなく、document内で完結します。

```ts:after
// NOTE: `taskDocRef` is `DocumentReference`.
taskDocRef.set({
  error: {
    code: 100,
    description: 'The operation could not be completed.'
  }
})
```

## document以下に値を追加したい/削除したい
RealtimeDBの場合、特定のrefに対して、set/update/removeを呼ぶ場合は以下のようになります。

```ts:before
// set
userRef.child('name').set('Taro')

// update
userRef.update({name: 'Taro'})

// remove
userRef.child('country').remove()
```

Firestoreの場合は、まず、Documentの持つFieldに対しては以下のようになります。

```ts:after
// NOTE: `userDocRef` is `DocumentReference`.

// set
userDocRef.set({name: 'Taro'})

// update
userDocRef.update({age: 28})

// remove
userDocRef.update({country: Firestore.FieldValue.delete()})
```

特筆すべきはremoveのときで、`remove()`が用意されていないので、`update`で、削除したいfieldのkeyの対して、 `Firestore.FieldValue.delete()`　を指定します。


Document自体の削除は、refに対して`delete()`を呼ぶだけです。

```ts:after(document)
// remove document
userRef.delete()
```

## フィールドの値の変更に関するトリガーがない
RealtimeDBの場合、特定のrefに対して`onCreate|Update|Delete|Write`を張ることができました。
例えば、`v1/user/{userID}/name`が **新たに** 追加されたタイミングで何かするのを仮定すると...

```ts:before
export const userNameAdded = functions.database.ref('/v1/user/{userID}/name').onCreate(async event => {
  const name = event.data.val()
  console.log(name)
})
```

こんな感じになります。

一方Firestoreの場合は、これがFieldに該当する訳ですが、Fieldの値の変更をトリガーにすることはできません。

```ts:after
// 注: これは正しくないコードです
export const userNameAdded = functions.firestore.ref('/version/1/user/{userID}/name').onCreate(async event => {
  const name = event.data.data()
  console.log(name)
})
```

ではどうやって、`user`ドキュメントに`name`が追加されたのをトリガーにして処理を書けば良いのでしょうか。
1つは、userドキュメントの`onUpdate`を拾い、`event.data`をゴニョゴニョ調べる方法があります。

### `event.data.previous` を見る
Firestoreにあって、RealtimeDBにない特徴として、`event.data`が`previous`を持つ場合があるというのが挙げられます。
もし、event.dataが更新されたりした場合には、その1つ前の状態が、`previous`プロパティに格納されます。
なので、先程の例で、user.nameが追加されたか調べるには、以下のようにします。

```ts:after
export const userNameAdded = functions.firestore.ref('/version/1/user/{userID}').onUpdate(async event => {
  const previousName =  event.data.previous && data.previous.data().name
  const currentName = event.data.data().name
  if (!previousName && currentName) {
    // user.nameが追加された
  }
})
```

ちなみに、`event.data.previous` は、onCreate等のタイミングではundefinedになるので、そのチェックを怠ると、 `data.previous.data()` でエラーになります。

...やや大変なのと、これを毎回書くのはしんどいですよね。

#### `event.data.data()`のあるkey(property)の変更があったかどうか判定する処理を作る
ということで、以下のような処理を準備して、`event.data.data()` のあるプロパティに関して前後で値がどう変化したか見れるようにします。

```ts:after
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
import { DeltaDocumentSnapshot } from 'firebase-functions/lib/providers/firestore'

export enum ValueChangesResult {
  none = 0,
  new = 1,
  updated = 2,
  removed = 3
}

export class ValueChanges {
  static for(key: string, data: DeltaDocumentSnapshot): ValueChangesResult {

    const currentValue = data.data()[key]
    const previousValue = data.previous && data.previous.data()[key]

    if (!previousValue && currentValue) {
      return ValueChangesResult.new
    }

    if (previousValue && !currentValue) {
      return ValueChangesResult.removed
    }

    if (currentValue === previousValue) {
      return ValueChangesResult.none
    } else {
      return ValueChangesResult.updated
    }
  }
}
```

先程のコードを変更するとこのようになります.

```ts:after
export const userNameAdded = functions.firestore.ref('/version/1/user/{userID}').onUpdate(async event => {
  if (ValueChanges.for('name', event.data) === ValueChangesResult.new) {
    // user.nameが追加された
  }
})
```

previousとcurrentの関係から、それぞれ`none`,`new`,`updated`,`removed`を判断します。

prev | current | (condition) | result
-----|---------|:------:|------
null | null | - | none
null | some | - | new
some | some | === | none
some | some | !== | updated
some | null | - | removed




### もう一つの解決策
これは設計次第になりますが、例えば`name`を例に取った場合は、以下のようにuserドキュメントから更にコレクションとドキュメントを生やすという手段もあります。

```ts:もう一つの解決策
export const userNameAdded = functions.firestore.ref('/version/1/user/{userID}/_name/{nameID}').onCreate(async event => {
  const name = event.data.data().name
  console.log(name)
})
```

こうすると、onCreateをトリガーに扱うことができるので、先程の方法より、関数の呼び出し数は減るのでだいぶマシにはなりそうです。
が、構造が深くなる問題もあるので、設計とコストとうまく見て判断したいところです。

この項についてよりベストプラクティスあるよって場合は**是非教えてください。**

## まとめ
RealtimeDBとfirestoreでそこまで大きく変化することはないですが、DBとしての特性が変わることにより、一部扱いやすくなる/にくくなる部分がでてくるので、移行していくときには十分注意が必要かと思います。
まだまだ僕含め開発チームでも日々取り組んでいるところではあるので、また知見が得られましたらぼちぼち共有しようかなと思います。

## 余談
本当は以前[firebase.yebisu#1](https://connpass.com/event/69238/)で発表した内容をより深掘りして投稿しようかなと思いましたが、最近触ってるホカホカなネタをと思い舵切りしました。

- [Try new Firebase AB testing](https://speakerdeck.com/sgrksmt/try-new-firebase-ab-testing)