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

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

前回の連載では、Firebase上に保存されたデータを用途に合わせてさまざまな方法で読み出してみましたが、データはすべてWebコンソールから用意していました。今回の連載では、クライアントアプリからさまざまな方法でデータをFirebase上に保存してみたいと思います。

今回も、前回同様、例として

していきたいと思います。

データの保存方法

Firebaeでデータを保存する方法は次のにある4つです。

メソッド概要
setValue()特定のURIにデータを直接保存
updateChildren()特定のデータの一部要素だけを更新
push()リストに時系列にデータを追加
runTransaction()複雑なデータをアトミックに保存するためのトランザクション

順番に確認していきましょう。

setValue()

setValue()は最も基本的なデータの保存方法です。指定したURIにデータを直接書き込みます。すでに同一パスにデータが存在した場合はすべての情報を上書きします。

setValue()には保存する任意のデータ型をそのまま指定できます。メッセージを表現するデータ型として以下のようなクラスを定義してください。

public class ChatMessage {
    public String body;
    public String sender;
    public long timestamp;

    public ChatMessage(String body, String sender, long timestamp) {
        this.body = body;
        this.sender = sender;
        this.timestamp = timestamp;
    }
}

あとはデータの読み出し同様、任意のURIへの参照を取得し、setValue()メソッドを呼び出して書き込むだけです。

Firebase messages = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/messages");
message.child("01").setValue(new ChatMessage("How are you doing?", "Steve", System.currentTimeMillis()));
message.child("02").setValue(new ChatMessage("I'm great!", "Bill", System.currentTimeMillis()));

メッセージ一覧への参照を取得した後、child("01")のようにして1つ目のメッセージを格納するためのキーを作り、その下にメッセージを直接保存しています。2つ目のメッセージも同様です。

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

画像

完了コールバック

もしsetValue()で保存が完了したタイミングで何か処理を差し込みたい場合は、setValue()の第2引数にFirebase.CompletionListener()を渡すことで完了後にコールバックしてもらえます。

messages.child("01").setValue(chatMessage, new Firebase.CompletionListener() {
    @Override
    public void onComplete(FirebaseError firebaseError, Firebase firebase) {
        if (firebaseError != null) {
            Log.e(TAG, firebaseError.getMessage(), firebaseError.toException());
        } else {
            Log.d(TAG, "data successfully saved!");
        }
    }
});

エラー発生時にはFirebaseErrorが渡されるので、それの有無で処理を切り分けるといいでしょう。

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

D/Firebase: data successfully saved!

updateChildren()

先ほどのsetValue()で大抵の場合は問題ないのですが、⁠送信者名だけを更新したい」といった一部のデータの更新にはsetValue()は向きません。setValue()は毎回すべての情報をまるごと書き換えてしまうからです。

このような用途にはupdateChildren()を利用します。

Map<String, Object> sender = new HashMap<>();
sender.put("sender", "Woz");
messages.child("01").updateChildren(sender);

このようにすると、senderだけをSteveからWozに書き換えることができます。bodytimestampはノータッチなので効率的です。

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のデータに適切なアクセス制御を施し、アプリケーションをよりセキュアに運用する方法について解説していきたいと思います。どうぞお楽しみに。

おすすめ記事

記事・ニュース一覧