code smith

開発で日々の生活をもっと楽しく

firestoreでtimestampカラム(createdAt,updatedAt)を自動付与する

f:id:gaku3601:20220102152520p:plain Gakuです。
最近はもっぱら、flutterとfirebaseで開発を行っています。 firestoreはNoSQLということもあって癖がありますが、最近ようやく慣れてきて爆速開発環境でニヤニヤしてます。 (個人開発ならこれくらいの環境でいいんだよ!

そんなfirestoreですが、ソートを実施するためや、リストの制限のために、documentの生成時・変更時にcreatedAtとupdatedAtを必ず付与する設計にしている人も多いのではないでしょうか?
createdAtとupdatedAtカラムをdocumentの生成時に必ず付与するようにしてもいいのですが、firestore ruleでcreatedAtとupdatedAtのルールを毎回付与するのも面倒ですし、 createdAtとupdatedAtがあることによって、upsertなメソッドを記載する時に嫌な書き方になることもあったので、firebase functionで一律で付与する設定にしてみました。

実際のコード

// 一階層目のcreatedAtの自動付与
export const createdCommonProcessNested1 = functions.firestore
    .document("{colId}/{colDocId}")
    .onCreate(async (snap) => {
      await fireStore.doc(snap.ref.path).set({
        createdAt: admin.firestore.FieldValue.serverTimestamp(),
        updatedAt: admin.firestore.FieldValue.serverTimestamp(),
      }, {merge: true});
    });

// 一階層目のupdatedAtの自動付与
export const updatedCommonProcessNested1 = functions.firestore
    .document("{colId}/{colDocId}")
    .onUpdate(async (snap) => {
      if (snap.before.data().updatedAt?._seconds !== snap.after.data().updatedAt._seconds) {
        return;
      }
      await fireStore.doc(snap.before.ref.path).set({
        updatedAt: admin.firestore.FieldValue.serverTimestamp(),
      }, {merge: true});
    });

// 二階層目のupdatedAtの自動付与
export const createdCommonProcessNested2 = functions.firestore
    .document("{colId}/{colDocId}/{subColId}/{subColDocId}")
    .onCreate(async (snap) => {
      await fireStore.doc(snap.ref.path).set({
        createdAt: admin.firestore.FieldValue.serverTimestamp(),
        updatedAt: admin.firestore.FieldValue.serverTimestamp(),
      }, {merge: true});
    });

// 二階層目のupdatedAtの自動付与
export const updatedCommonProcessNested2 = functions.firestore
    .document("{colId}/{colDocId}/{subColId}/{subColDocId}")
    .onUpdate(async (snap) => {
      if (snap.before.data().updatedAt?._seconds !== snap.after.data().updatedAt._seconds) {
        return;
      }
      await fireStore.doc(snap.before.ref.path).set({
        updatedAt: admin.firestore.FieldValue.serverTimestamp(),
      }, {merge: true});
    });

こんな感じでfirebase functionsに記載しておくことでcreatedAtとupdatedAtを自動で付与・更新してくれます。 ポイントとしては

export const createdCommonProcessNested1 = functions.firestore
    .document("{colId}/{colDocId}")

.document("{colId}/{colDocId}")の部分でwildcardを指定することができないため、階層毎にメソッドを定義してあげる必要があります。
(今回の場合1階層目のdocumentと2階層目のdocumentを生成・更新した際にcreatedAtとupdatedAtを更新するようになります。 なので3階層目などで自動付与を実施する場合、別途メソッドを定義してあげる必要があります。

また、update時

documentのupdateを実施する→updatedAtの更新を実施するメソッドを実行→updatedAtの更新を実施するメソッドを実行→・・・∞

というようにupdateを検知してupdatedAtの更新を実施しているので、何もしないと∞ループに陥ってしまいます。 なので

if (snap.before.data().updatedAt?._seconds !== snap.after.data().updatedAt._seconds) {
        return;
}

の部分で、更新時にupdatedAtを更新したか検知し、updatedAtを更新した(上記で定義したupdatedAtの更新メソッドで更新)場合、処理を終了するようにしています。

おわり

以上です。この設定しておけば、createdAtとupdatedAtの概念を考えずに実装できるようになるので、とりあえず設定しておけば幸せになります。