はじめに
先日リリースしたFlutter×Firebase製アプリ 「カラオケメモアプリ-うためも」が機能アップデートをしました🎉
ユーザーの方からリクエストが多かった、「フォルダ機能」になります。
今回は、フォルダ機能の開発にあたって大変だったことと対応手順をまとめていきます。
⬇️「カラオケメモアプリ-うためも」はこちらからインストールできます
App Store: https://apps.apple.com/us/app/id1527778412
Google Play: https://play.google.com/store/apps/details?id=com.taikishiino.uta_memo
フォルダ機能アップデートにあたって
全ユーザーのコレクション配下に新しくfoldersサブコレクションを書き込むスクリプトを流す必要がありました。
スクリプト処理としては、usersコレクション(/users
)のドキュメント一覧を取得した後、各ドキュメントにfoldersサブコレクション(/users/userId/folders/folderId
)に書き込むといった流れになります。
Firestore設計ミス
usersコレクションの全てのドキュメントが、「サブコレクションはあるがフィールドが空のドキュメント」になっていました。
これの何が悪いのかというと、ドキュメント一覧を取得する以下のようなクエリで取得できないという点です。(空扱いになるらしい)
db.collection("users").get()
そのため、当初の予定だった「usersコレクション(/users
)のドキュメント一覧を取得した後、各ドキュメントにfoldersサブコレクション(/users/userId/folders/folderId
)に書き込む」ができないのです。
解決方法
usersコレクション配下の全てのドキュメントにフィールドを書き込んだのち、新しいサブコレクションを書き込むスクリプトを流すといった少々強引な方法で行いました。
全てのドキュメントにフィールドを書き込む
以下のCloud Funtions
をデプロイした後に、Goスクリプトを流すことで全てのドキュメントにフィールドを書き込むことに成功しました。
・Cloud Funtions
サブコレクションへの書き込みをトリガーに、親のドキュメントRefにupdatedAtフィールドに書き込むTypeScript
のファンクションを用意しました。
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
const serviceAccount = require("../xxxxx.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://xxxxxxx.firebaseio.com"
});
export const SyncSongsToUsers = functions.firestore
.document('users/{userId}/songs/{songId}')
.onWrite(async (change: any, context: any) => {
const uid = context.params.userId;
const newVal = change.after.data();
const updatedAt = newVal.updatedAt;
const queryUser = admin.firestore().collection(`users`).doc(uid);
try {
const doc = await queryUser.get();
if (doc.exists) {
await queryUser.update({
updatedAt: updatedAt.toDate()
});
} else {
await queryUser.set({
createdAt: updatedAt.toDate(),
updatedAt: updatedAt.toDate(),
deletedAt: null
});
}
} catch(e) {
console.log("ERROR Set or Update =====>", e);
}
return null
});
・Goスクリプト
上記で設定したサブコレクションusers/{userId}/songs/{songId}
パスに対してのコレクショングループを使い直接UPDATE処理をするスクリプトを流します。
func main() {
ctx := context.Background()
firestoreClient, err := connectFirestore(ctx)
if err != nil {
fmt.Println("firebase Connection error: ", err)
}
defer firestoreClient.Close()
it := firestoreClient.CollectionGroup("songs").Documents(ctx)
for {
doc, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
fmt.Println("err\n", err)
}
song := &Song{}
err = doc.DataTo(song)
if err != nil {
return
}
doc.Ref.Set(ctx, map[string]interface{}{
"folderId": xxxx,
}, firestore.MergeAll)
}
fmt.Println("----- main end -----")
}
func connectFirestore(ctx context.Context) (*firestore.Client, error) {
sa := option.WithCredentialsFile("../xxxxx.json")
app, err := firebase.NewApp(ctx, nil, sa)
if err != nil {
fmt.Printf("error initializing app: %v", err)
return nil, err
}
return app.Firestore(ctx)
}
foldersサブコレクションを書き込むスクリプトを流す
以下を流して、usersのコレクション配下に新しくfoldersサブコレクションを書き込むことができました。
func main() {
ctx := context.Background()
firestoreClient, err := connectFirestore(ctx)
if err != nil {
fmt.Println("firebase Connection error: ", err)
}
defer firestoreClient.Close()
iter := firestoreClient.Collection("users").Documents(ctx)
for {
doc, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
fmt.Println("err\n", err)
}
it := doc.Ref.Collection("folders").Documents(ctx)
flg := true
for {
folderDoc, err := it.Next()
if err == iterator.Done {
break
}
if doc.Data() != nil {
flg = false
}
}
if flg == true {
uid := doc.Ref.ID
now := time.Now()
_, _, err = firestoreClient.
Collection("users").
Doc(uid).
Collection("folders").
Add(ctx, map[string]interface{}{
"name": "未分類",
・
・
"createdAt": now,
"updatedAt": now,
"deletedAt": nil,
})
if err != nil {
fmt.Println("err\n", err)
}
}
}
fmt.Println("----- main end -----")
}
func connectFirestore(ctx context.Context) (*firestore.Client, error) {
sa := option.WithCredentialsFile("../xxxxx.json")
app, err := firebase.NewApp(ctx, nil, sa)
if err != nil {
fmt.Printf("error initializing app: %v", err)
return nil, err
}
return app.Firestore(ctx)
}
最後に
業務では選択しないであろう強引な方法でしたが、勉強にもなったのでよかったかなと思っています。。
のちに気付いたのですが、「サブコレクションの親ドキュメントRefを取得する方法」もあるらしく、わざわざCloud Funtions使わなくてもよかったなと少し後悔しています。。
- Firestore - サブコレクションの親ドキュメントを取得する https://www.366service.com/jp/qa/c4683e67acd060cba46d0feb09013b5b
Firestoreの設計は、クエリドリブンなデータベース設計をするのが良いと感じました。 その中でも、今回のようなアンチパターン対策として、「サブコレクションがあるドキュメントにも何かしらのフィールドは作る」ようにしましょう。