やりたいこと
FlutterからFunctionsを呼び出して、500件以上のfirestoreドキュメント(Timestamp含む)の登録、更新、削除を行いたい。
経緯
現状の実装が、FlutterからFiresoreに対してループして1件ずつ更新処理を実行していた。
トランザクション管理をしていないので、処理中にアプリ強制終了すると中途半端な更新となる事象が発生。
FlutterからFirestoreに対して直接更新のまま、FirestoreのBatch処理を利用したかったが、Batchのトランザクション上限が500件なので、500件以上の更新があると全ロールバックができない。
そのため、FlutterからFunctionsを呼び出し、FunctionsからFiresoreを更新させる。
この場合も正確にはトランザクション担保できていないが、Functionsであればアプリ強制終了の影響は受けないので、こちらで対応するように判断した。
なおforEachはawaitが使えないので注意。(処理完了前にFlutterにreturnが来て悩んだ)
対応前の実装
Flutter(レポジトリ)
レポジトリの登録メソッド内でリストを受け取って、単純setを繰り返して登録している。ドメイン(FirestoreXxx)内にtoFirestore()
メソッドを持ちDateTimeをTimestampに変換している。
トランザクション管理をしていないので、処理の途中でアプリを強制終了すると途中まで更新された状態になる。
Future<void> createXxxs(
List<FirestoreXxx> xxxs) async {
final now = DateTime.now();
await Future.forEach(xxxs, (FirestoreXxx xxx) async {
final doc = _storeDB
.collection('xxx')
.doc();
xxx.xxxId = doc.id;
xxx.createdAt = DateTime.now();// ここはあるべき業務ロジックを
await doc.set(xxx.toFirestore());
}
});
}
対応後の実装
Flutter(レポジトリ)
直接Firestoreに対して登録せずに、jsonにしてFuncstionsに渡す。
ドメイン(FirestoreXxx)内に新しく追加するtoJson()
メソッドを呼にでjson変換している。
Future<dynamic> createXxxs(List<FirestoreXxx> xxxs) async {
List<Map<String, dynamic>> xxxsJson = [];
await Future.forEach(xxxs, (FirestoreXxx xxx) async {
xxx.createdAt = DateTime.now();// ここはあるべき業務ロジックを
xxxsJson.add(xxx.toJson());
}
});
final callable = FirebaseFunctions.instanceFor(
app: Firebase.app(),
region: 'asia-northeast1',
).httpsCallable('xxx-create');
return await callable.call({
'xxxList': json.encode(xxxsJson),
});
}
Flutter(ドメイン)
ドメインのtoJsonでTimestampはミリ秒に変換しておく。
Functions側でTimestampに変換しやすいので。
Map<String, dynamic> toJson() {
return {
'createdAt': createdAt.millisecondsSinceEpoch,//ミリ秒にする
};
}
Functions
JSON.parseの第2引数(reviver)でミリ秒からTimestampに変換するのがポイント。
変換しないとFirestoreに文字列で登録されてしまう。
またチャンク処理で500件ごとにBatch更新を行う。
500件を超えると、正確なトランザクションではないが、ちゃんと入力チェックをすれば処理中にエラーで落ちることはほぼ無く、ミッションクリティカルな処理でなければこれで十分と判断。
import * as functions from "firebase-functions";
import * as firebaseAdmin from "firebase-admin";
import * as lodash from "lodash";
export const create =
functions.region("asia-northeast1").https.onCall(async (data, context) => {
if (context.auth == null) return;
const db = firebaseAdmin.firestore();
// 登録用type。時刻はFirestoreに登録するためにTimestampとして定義
type Xxx = {
createdAt: firebaseAdmin.firestore.Timestamp
};
const xxxList =
await JSON.parse(data. xxxList, function(key, value) {
// timestampはミリ秒で渡されるので変換する
const timestamps = ["createdAt"];// サンプルなのでcreateAtのみだが、他にあれば配列に追加
if ( timestamps.includes(key) ) {
// ミリ秒からTimestampに変換する処理
return firebaseAdmin.firestore.Timestamp.fromMillis(value);
}
return value;
}) as [Xxx];// asでTypeを指定
// firestoreの仕様でbatchは500件までなのでチャンクする
// forEachにするとawaitが使えないので注意
for (const xxxs of lodash.chunk(xxxList, 500)) {
const batch = db.batch();
xxxs.forEach((xxx) =>{
const doc = db.collection("xxxs").doc();
xxx.xxxId = doc.id;
batch.set(doc, xxx);
});
await batch.commit();
}
});
参考
https://qiita.com/superman9387/items/6623017ef9609308c6fe
https://zenn.dev/sgr_ksmt/articles/b5fa4f8b9e0a33ccf609
https://dev.classmethod.jp/articles/foreach-async-await/
感想
FirestoreのBatch処理が500上限が緩和される日は来るのだろうか。。