スマホアプリ開発を加速する,Firebaseを使ってみよう

第4回 データの保存をマスターする

この記事を読むのに必要な時間:およそ 4 分

push()

メッセージ一覧のようなデータを作成するとき常に意識しておかなければならないことは,Firebaseは複数クライアントによって常に同時に読み書きが発生する可能性があるという点です。

最初にsetValue()で例示したような,/messages/01,messages/02のように連番を手動で追加するようなコードでは,複数ユーザによる同時書き込みで簡単に衝突が発生してしまい,データを正しく書き込むことができません。このような場合に便利なのがpush()メソッドです。

push()メソッドを呼び出すと,Firebaseがタイムスタンプに基づいた一意なIDを自動的に払い出してくれます。これを利用することで,ユーザは手動で連番を発行したりせずとも安全にリストに要素を追加していくことができます。

IDはランダムな文字列ですが,先述の通りタイムスタンプに基づいているため,デフォルトで時間軸で昇順に並んでおり扱いやすくなっています。

messages.push().setValue(new ChatMessage("I can't hear you!", "Hartman", System.currentTimeMillis()));
messages.push().setValue(new ChatMessage("Sir, yes sir!", "Lawrence", System.currentTimeMillis()));

この通り,Firebaseの参照とsetValue()の間にpush()と挟むだけで簡単に利用できます。

Webコンソールで確認して,以下のようなデータが保存されていれば成功です。

画像

もし別の場所で参照するためにこの一意なIDが必要な場合は,以下のようにすることで簡単に取得できます。

Firebase newPostRef = messages.push();
newPostRef.setValue(new ChatMessage("Is this me?", "Joker", System.currentTimeMillis()));
String key = newPostRef.getKey();
Log.d(TAG, "key: " + key);

ログに以下のように出力されれば成功です。

D/Firebase: key: -KGD-cmiz_I0ZKOC_Fx8

runTransaction()

先ほども申し上げたように,Firebaseでは常にデータが同時に書き込まれても大丈夫なように設計しておくことが非常に肝要です。

たとえば,あるメッセージに複数ユーザが同時にいいね!ボタンを押すと,いいね数のカウンターが1つずつカウントアップされる機能を実装するとしましょう。こういった機能には,同時書き込みでも正しくカウントアップできるようにトランザクション処理をすることが不可欠です。

FirebaseではrunTransaction()メソッドで簡単にトランザクション処理を行うことができます。

まずはコードを見て下さい。それから順に解説していきます。

Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/counter");
ref.runTransaction(new Transaction.Handler() {
    @Override
    public Transaction.Result doTransaction(MutableData mutableData) {
        if (mutableData.getValue() == null) {
            mutableData.setValue(1);
        } else {
            Long counter = mutableData.getValue(Long.class);
            counter++;
            mutableData.setValue(counter);
        }
        return Transaction.success(mutableData);
    }

    @Override
    public void onComplete(FirebaseError firebaseError, boolean committed, DataSnapshot dataSnapshot) {
        if (committed) {
            String logMessage = dataSnapshot.getValue().toString();
            Log.d(TAG, "counter: " + logMessage);
        } else {
            Log.e(TAG, firebaseError.getMessage(), firebaseError.toException());
        }
    }
});

まず,いつものようにFirebaseの参照を取得し,runTransaction()メソッドを呼び出します。

runTransaction()にはTransaction.Handler()インスタンスを引数として渡します。

前半のpublic Transaction.Result doTransaction(MutableData mutableData) { }の部分が,トランザクション処理の本体であり,後半のpublic void onComplete(FirebaseError firebaseError, boolean committed, DataSnapshot dataSnapshot) { }の部分がトランザクション完了後の処理を記述する部分になります。

doTransaction()に渡されるMutableDataは,トランザクションでアトミックに扱われるデータそのものであり,任意のデータ型で構いません。初回トランザクション時にはこの値は空である可能性があるため,必ずnullチェックし,初期値をセットする必要があります。

既に値がある場合はMutableData#getValue()でそれを取り出すことができます。ここではクラス型を指定して型安全に取得可能です。今回の例ではカウンターなので整数型で取り出しています。

最後にTransaction.success(mutableData)で成功のトランザクションとして次の処理に結果を渡しています。

onComplete(FirebaseError firebaseError, boolean committed, DataSnapshot dataSnapshot)では,トランザクションのコミットが成功したかどうかが第2引数の真偽値でわかるので,成功した場合はその値を取り出してログに書き出し,失敗した場合は同様にエラーをログ出力しています。

すべての処理が完了すると,ログに以下のようにカウントアップされた数値が出力されれば成功です。

D/Firebase: counter: 12

オフライン時の挙動

前回でも少し触れましたが,Firebaseではデータの書き込みはまずローカルのデータベースに反映され,その後リモートにあるFirebaseの中央サーバと同期されます。

もし,何らかの理由でインターネット接続が切断されていても,ローカルへの書き込みは成功するため,ユーザはオンラインかオフラインかをほとんど気にすることなくアプリケーションを利用し続けることができます。その後,ネットワーク接続が回復したら順次バックグラウンドでデータが同期されます。

このことで,アプリケーション開発者は

  • 「もしオフラインならばダイアログを表示してユーザを待たせて…」
  • 「もしネットワークが回復したらリトライ処理をして…」

といった複雑な処理を記述する必要がなく,アプリケーション開発に専念することができます。これこそがFirebase開発の長所であり醍醐味と言えるでしょう。

まとめ

今回までの連載で,Firebase上のデータベースに任意のデータを読み書きし,アプリケーション側ではそのデータの変更に合わせてリアルタイムに処理を反映することで,リッチでダイナミックなアプリケーション開発ができることをひと通り見てきました。簡単なアプリケーションならば,もういまの知識だけでもかなり自由に作ることができるはずです!

さて,次回の連載では,Firebaseのデータに適切なアクセス制御を施し,アプリケーションをよりセキュアに運用する方法について解説していきたいと思います。どうぞお楽しみに。

著者プロフィール

白山文彦(しろやまふみひこ)

サーバサイド,インフラ,Androidなど何でもやるプログラマ。