Firebaseのデータベースのアクセスは非同期

記事
IT・テクノロジー

Firebaseのデータベースのアクセスは非同期

Firebaseの多くのアクセスはネットワークを介する物が多いので非同期の処理になるものが多くなります。
非同期の処理は慣れないと分かりにくい所もあって最初は苦労する部分です。この記事では、Firebaseのデータベースの非同期処理について少し詳しく解説しています。

Firebase Cloud Firestoreデータベースのアクセスの基本

FirebaseのCloud Firestoreのデータベースの基本は
コレクション
ドキュメント
この2つです。コレクションはドキュメントを入れておく入れ物のようなもので、例えば「address(住所録)」のコレクションを作って、その中に各個人の住所などの情報を入れたドキュメントを格納して行くように使います。
一つ一つのドキュメントのデータはJSON形式のデータになります。
const refAddress =firebase.firestore().collection

がコレクションへのリファレンスになります。要はコレクション「address」の場所を示しています。この中にドキュメントが格納されます。
新たにドキュメントを加える場合
const personA = {
    address: "XXX県YYY市3丁目3番地",
    name:"山田太郎"
}
firebase.firestore().collection("address").add(personA).then((doc) => {
    // 追加に成功
    console.log("成功");
}).catch((error) => {
    // 追加に失敗
    console.log("失敗");
})
consle.log("End");

の様に書きます。 Firebaseのドキュメントを見ると似たような例が沢山あります。
この記述の「then」や「catch」の部分が非同期に処理される部分です。 このプログラムを実行すると殆どの場合

End
成功

のような順番で表示されます。「console.log("End")」の方が先に実行される場合が殆どだと思います。
これは、Firebaseの処理はネットワークを経由して行われるため少し時間がかかるので、先に「console.log("End")」を実行して、firebaseの処理が終わって、「doc」の準備ができた時点で、「then」の中の関数(アロー関数)が実行されるためです。この仕組みをコールバック関数と呼んでいます。
初心者の人は、書いた順番で実行されない気がするので混乱しやすい部分です。
Firebaseのデータベースのアクセスは基本的にこのような形で行われます。
この「doc」の正体は何かというと、Firebaseに追加した「ドキュメントのデータ」になります。実際に追加した「parsonA」もここに含まれていますが、その他にFirebaseがデータを加えた物になります。例えば「ドキュメントのID」がこの中に含まれています。なので、Firebaseの処理が終わるまでは分からないデータです。
同じような処理をする書き方には次のような書き方も可能です。
const const personA = {
    address: "XXX県YYY市3丁目3番地",
    name:"山田太郎"
}
add(personA)
async function add(person) {
    try {
        const doc = await firebase.
                        firestore().
                        collection("address").
                        add(person);
        console.log("成功");
    } catch (error) {
        console.log("失敗");
    }
    consle.log("End")
}

やっている処理は殆ど同じですが、この場合表示は
成功
End
になります。
Firebaseの処理に「await」がついているので、Firebaseの処理が終わるまで待って実行されます。そのためには、関数に「async」をつけて、非同期対応の処理をすると宣言しないといけません。
Javascriptに慣れていない場合、こちらの書き方の方が分かり易い場合が多いのではないでしょうか?

書き込みより読み込みの方が厄介!

書き込みの場合は、そのデータを使うケースは少ない場合が多いのですが、読み込みの場合は読み込んだデータを使う場合が殆どなのでこの非同期の処理は意外に厄介です。
例えば、同じようなコレクション「address」に入っているドキュメントを全部取り出して表示しようとする場合を考えてみます。
getAddressList() {
    firebase.firestore().
        collection("address").
        get().
        then((snapshot) => {
            const list = [];
            snapshot.forEach((doc) => {
                // 住所と名前を表示
                console.log(doc.data().name);
                console.log(doc.data().address);
                list.push(doc.data())
            });
    });
}
これで、コレクション「address」に入っている人の名前と住所が全部表示されます。全てのデータは、「list」に格納されているのですが、この処理は「forEach」と別の非同期処理が行われているので、この、「then」の内側で処理する分には余り問題はないのですが、このFirebaseの処理の外側で「list」のデータを使おうとするとちょっと不便です。
例えば、Reactのステートでこの住所のリストを管理しようとした場合は結構厄介です。
getAddressList() {
    firebase.firestore().
        collection("address").
        get().
        then((snapshot) => {
            const list = [];
            snapshot.forEach((doc) => {
                // 住所と名前を表示
                console.log(doc.data().name);
                console.log(doc.data().address);
                list.push(doc.data());
            });
            this.setState({
                addresses: list
            })
    });
}
の様にしてしまうと、正しくステートが更新されない場合があります。 このような記述をしたい場合は、非同期のコールバックを使わずに普通に「for文」で書いた方が安全です。
getAddressList() {
    firebase.firestore().
        collection("address").
        get().
        then((snapshot) => {
            const list = [];
            for(let i = 0 ; i < snapshot.size ; i++) {
                // 住所と名前を表示
                console.log(doc.data().name);
                console.log(doc.data().address);
                list.push(snapshot.docs[i].data());
            }
            this.setState({
                addresses: list
            })
    }).catch((error) => {
        this.setState({
            address:[]
        })
    });
}
の様に書いた方が安全で確実にステートが更新されます。通常のForループは同期で処理されるので、ステートをセットする時は全てのループの処理が終わっているからです。
別の書き方としては、「Promise」を使う方法があります。
getAddressList() {
    return new Promise((resolve) => {
        firebase.firestore().
            collection("address").
            get().
            then((snapshot) => {
                const list = [];
                for(let i = 0 ; i < snapshot.size ; i++) {
                    // 住所と名前を表示
                    console.log(doc.data().name);
                    console.log(doc.data().address);
                    list.push(snapshot.docs[i].data());
                }
                resolve(list);
        }).catch((error) => {
            resolve([]);
        });
    });
}
のように書けば、通常の同期処理を行うメソッドと同じように扱うことができます。ただし呼び出しの方には、「await」をつける事と、呼び出し元の関数やメソッドには「async」をつける必要があります。

React やVueで使う場合は比較的問題が少ない

ReactやVueでは、表示を書き換える(レンダリング)するのに、「ステート」という概念を使っているので、ステートが変わると書き換えてくれるので、直接書き換えの処理を呼び出す必要がないので、ステートだけをきちんと管理できれば正常に動作します。
先ほどの例で説明したように、ステートを更新する場所と条件だけ気をつければ正常に動作します。

どうやって勉強すれば良いか?

こうした非同期の処理は本を読んだり、説明を受けるだけではどうしてもわからない部分があります。この挙動を理解するには作って試すのが一番です。とにかく実装をして、いろいろな条件で動かしてみて思った通りに表示されるかを調べるのが理解を深めるコツです。

まとめ

Firebaseを使った実装は便利ですが、非同期の処理を多用します。 早く慣れるには、実際に作って試すのが一番です。
一つのお勧めの題材が、「お問合せフォーム(送信)」と「管理アプリ」です。 お問合せフォームの送信のアプリでは、Firebaseのデータベースにお問合せ内容を登録して、管理アプリではお問合せの一覧を表示したり、お問合せの削除の機能をつけて、削除の際にはリストの更新をするような機能を実装します。
これを一通り自分で作ってみると、基本的なFirebaseのデータベースの使いかたが分かるようになります。
この制作例は、別に記事で紹介予定です。
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す ココナラコンテンツマーケット ノウハウ記事・テンプレート・デザイン素材はこちら