データの保存

このドキュメントでは、Firebase Realtime Database にデータを書き込むための 4 つの方法(set、update、push、およびトランザクション機能)について説明します。

データの保存方法

set messages/users/<username> など、定義済みのパスへのデータの書き込みや、データの置換を行います。
update データのすべてを置換することなく、定義済みのパスのキーの一部を更新します。
push データベース内のデータのリストに追加します。新しいノードをリストにプッシュするたびに、データベースから messages/users/<unique-user-id>/<username> のような一意のキーが生成されます。
transaction 同時更新によって破損する可能性がある複合データで作業する場合は、トランザクション機能を使用します。

データの保存

基本的なデータベース書き込み操作は、新しいデータを指定したデータベース参照に保存し、そのパスにある既存のデータを置き換える操作である set です。set を理解するために、簡単なブログアプリを構築します。アプリ用のデータはこのデータベース参照に保存されます。

Java
final FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("server/saving-data/fireblog");

Node.js
// Import Admin SDK
var admin = require("firebase-admin");

// Get a database reference to our blog
var db = admin.database();
var ref = db.ref("server/saving-data/fireblog");

いくつかのユーザーデータを保存するところから始めましょう。一意のユーザー名を使用して各ユーザーを保存します。また、ユーザーの氏名と生年月日も保存します。ユーザーごとに一意のユーザー名を持つため、ここでは push メソッドではなく set メソッドを使用するのが理に適っています。キーを既に持ち、作成する必要がないからです。

まず、ユーザーデータへのデータベース参照を作成します。次に、set() / setValue() を使用してユーザー オブジェクトをデータベースに、ユーザーのユーザー名、氏名、生年月日とともに保存します。set には文字列、数字、ブール値、null、配列または任意の JSON オブジェクトを渡すことができます。null を渡すと、指定した場所にあるデータが削除されます。この場合は、オブジェクトを渡します。

Java
public static class User {

    public String date_of_birth;
    public String full_name;
    public String nickname;

    public User(String date_of_birth, String full_name) {
        // ...
    }

    public User(String date_of_birth, String full_name, String nickname) {
        // ...
    }

}


DatabaseReference usersRef = ref.child("users");

Map<String, User> users = new HashMap<String, User>();
users.put("alanisawesome", new User("June 23, 1912", "Alan Turing"));
users.put("gracehop", new User("December 9, 1906", "Grace Hopper"));

usersRef.setValue(users);

Node.js
var usersRef = ref.child("users");
usersRef.set({
  alanisawesome: {
    date_of_birth: "June 23, 1912",
    full_name: "Alan Turing"
  },
  gracehop: {
    date_of_birth: "December 9, 1906",
    full_name: "Grace Hopper"
  }
});

JSON オブジェクトがデータベースに保存されると、オブジェクトのプロパティがデータベースの子の場所にネスト式に自動マッピングされます。ここで、URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name に移動すると、値 "Alan Turing" が表示されます。また、データを子の場所に直接保存することもできます。

Java
usersRef.child("alanisawesome").setValue(new User("June 23, 1912", "Alan Turing"));
usersRef.child("gracehop").setValue(new User("December 9, 1906", "Grace Hopper"));

Node.js
usersRef.child("alanisawesome").set({
  date_of_birth: "June 23, 1912",
  full_name: "Alan Turing"
});
usersRef.child("gracehop").set({
  date_of_birth: "December 9, 1906",
  full_name: "Grace Hopper"
});

上記の 2 つの例、つまり、両方の値をオブジェクトと同時に書き込む場合と、子の場所に別個に書き込む場合のでは、結果として同じデータがデータベースに保存されることになります。

{
  "users": {
    "alanisawesome": {
      "date_of_birth": "June 23, 1912",
      "full_name": "Alan Turing"
    },
    "gracehop": {
      "date_of_birth": "December 9, 1906",
      "full_name": "Grace Hopper"
    }
  }
}

最初の例では、データを監視しているクライアントで 1 つのイベントのみがトリガーされますが、2 番目の例では 2 つのイベントがトリガーされます。データがパス usersRef に既に存在する場合、最初の方法ではデータが上書きされることに注意してください。これに対し、2 番目の方法では、個々の子ノードの値のみが変更され、usersRef の他の子は変更されません。

保存済みデータの更新

他の子ノードを上書きすることなく、データベースの場所にある複数の子に同時に書き込むには、次に示す update メソッドを使用できます。

Java
DatabaseReference hopperRef = usersRef.child("gracehop");
Map<String, Object> hopperUpdates = new HashMap<String, Object>();
hopperUpdates.put("nickname", "Amazing Grace");

hopperRef.updateChildren(hopperUpdates);

Node.js
var hopperRef = usersRef.child("gracehop");
hopperRef.update({
  "nickname": "Amazing Grace"
});

これにより、Grace のデータが更新されてニックネームが含まれるようになります。update の代わりに set を使用した場合、full_namedate_of_birth の両方が hopperRef から削除されています。

Firebase Realtime Database では、マルチパスの更新もサポートされています。つまり、update では、データベース内の複数の場所にある値を同時に更新できます。これは、データの非正規化に役立つ強力な機能です。マルチパスの更新を使用すると、Grace と Alan に同時にニックネームを追加できます。

Java
Map<String, Object> userUpdates = new HashMap<String, Object>();
userUpdates.put("alanisawesome/nickname", "Alan The Machine");
userUpdates.put("gracehop/nickname", "Amazing Grace");

usersRef.updateChildren(userUpdates);

Node.js
usersRef.update({
  "alanisawesome/nickname": "Alan The Machine",
  "gracehop/nickname": "Amazing Grace"
});

この更新後は、Alan と Grace の両方にニックネームが追加されています。

{
  "users": {
    "alanisawesome": {
      "date_of_birth": "June 23, 1912",
      "full_name": "Alan Turing",
      "nickname": "Alan The Machine"
    },
    "gracehop": {
      "date_of_birth": "December 9, 1906",
      "full_name": "Grace Hopper",
      "nickname": "Amazing Grace"
    }
  }
}

パスを含めてオブジェクトを書き込むことでオブジェクトを更新すると、異なる動作が発生することに注意してください。この方法で Grace と Alan を更新するとどうなるでしょうか。

Java
Map<String, Object> userNicknameUpdates = new HashMap<String, Object>();
userNicknameUpdates.put("alanisawesome", new User(null, null, "Alan The Machine"));
userNicknameUpdates.put("gracehop", new User(null, null, "Amazing Grace"));

usersRef.updateChildren(userNicknameUpdates);

Node.js
usersRef.update({
  "alanisawesome": {
    "nickname": "Alan The Machine"
  },
  "gracehop": {
    "nickname": "Amazing Grace"
  }
});

この結果、異なる動作が発生します。つまり、/users ノード全体が上書きされます。

{
  "users": {
    "alanisawesome": {
      "nickname": "Alan The Machine"
    },
    "gracehop": {
      "nickname": "Amazing Grace"
    }
  }
}

完了コールバックの追加

データがいつ commit(確定)されたのかを把握したい場合は、完了コールバックを追加できます。set と update はどちらも、完了コールバックをオプションとして取ります。このコールバックは、書き込みがデータベースに commit されたときに呼び出されます。なんらかの理由で呼び出しが失敗した場合は、失敗した理由を示すエラー オブジェクトがコールバックに渡されます。

Java
DatabaseReference dataRef = ref.child("data");
dataRef.setValue("I'm writing data", new DatabaseReference.CompletionListener() {
    @Override
    public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
        if (databaseError != null) {
            System.out.println("Data could not be saved " + databaseError.getMessage());
        } else {
            System.out.println("Data saved successfully.");
        }
    }
});

Node.js
dataRef.set("I'm writing data", function(error) {
  if (error) {
    alert("Data could not be saved." + error);
  } else {
    alert("Data saved successfully.");
  }
});

データのリストの保存

データのリストを作成するときは、大半のアプリケーションが本質的にマルチユーザー アプリケーションであることから、それに応じてリストの構造を調整する必要があるので注意してください。上記の例を拡張し、アプリにブログ投稿を追加してみましょう。最初は set を使用して次のように、自動増分式の整数インデックスとともに子を保存しようとするかもしれません。

// NOT RECOMMENDED - use push() instead!
{
  "posts": {
    "0": {
      "author": "gracehop",
      "title": "Announcing COBOL, a New Programming Language"
    },
    "1": {
      "author": "alanisawesome",
      "title": "The Turing Machine"
    }
  }
}

ユーザーが新しい投稿を追加すると、その投稿は /posts/2 として保存されます。これがうまくいくのは 1 人の作成者が投稿を追加する場合だけです。一方、コラボレーションに対応したブログ アプリケーションでは、多数のユーザーが同時に投稿を追加する可能性があります。2 人の作成者が同時に /posts/2 に書き込むと、投稿の 1 つがもう 1 つの投稿によって削除されます。

これを解決するために、Firebase クライアントには push() 関数が用意されています。この関数は新しい子ごとに一意のキーを生成します。一意の子キーを使用すると、書き込み競合を心配することなく、複数のクライアントが同時に同じ場所に子を追加できます。

Java
public static class Post {

    public String author;
    public String title;

    public Post(String author, String title) {
        // ...
    }

}


DatabaseReference postsRef = ref.child("posts");

DatabaseReference newPostRef = postsRef.push();
newPostRef.setValue(new Post("gracehop", "Announcing COBOL, a New Programming Language"));

// We can also chain the two calls together
postsRef.push().setValue(new Post("alanisawesome", "The Turing Machine"));

Node.js
var postsRef = ref.child("posts");

var newPostRef = postsRef.push();
newPostRef.set({
  author: "gracehop",
  title: "Announcing COBOL, a New Programming Language"
});

// we can also chain the two calls together
postsRef.push().set({
  author: "alanisawesome",
  title: "The Turing Machine"
});

一意のキーはタイムスタンプに基づいているため、リストアイテムは自動的に時系列に並べ替えられます。 ブログ投稿ごとに一意のキーを生成するため、複数のユーザーが投稿を同時に追加しても書き込み競合は発生しません。これで、データベースのデータは次のようになります。

{
  "posts": {
    "-JRHTHaIs-jNPLXOQivY": {
      "author": "gracehop",
      "title": "Announcing COBOL, a New Programming Language"
    },
    "-JRHTHaKuITFIhnj02kE": {
      "author": "alanisawesome",
      "title": "The Turing Machine"
    }
  }
}

JavaScript では、push() を呼び出した直後に set() を呼び出すパターンが一般的であるため、次のように、set するデータを直接 push() に渡すと、これらをまとめることができます。

Java
// No Java equivalent
Node.js
// This is equivalent to the calls to push().set(...) above
postsRef.push({
  author: "gracehop",
  title: "Announcing COBOL, a New Programming Language"
});

push() で生成される一意のキーの取得

push() を呼び出すと、新しいデータパスへの参照が返されます。これを使用して、キーの取得またはキーに対するデータの設定を実行できます。次のコードを実行すると、上記の例と同じデータを得ることができますが、今度は、生成された一意のキーにアクセスできるようになります。

Java
// Generate a reference to a new location and add some data using push()
DatabaseReference pushedPostRef = postsRef.push();

// Get the unique ID generated by a push()
String postId = pushedPostRef.getKey();

Node.js
// Generate a reference to a new location and add some data using push()
var newPostRef = postsRef.push();

// Get the unique key generated by push()
var postId = newPostRef.key;

ご覧のとおり、push() 参照から一意のキーの値を取得できます。

データの取得に関する次のセクションでは、Firebase データベースからこのデータを読み取る方法について説明します。

トランザクション データの保存

増分カウンタなど、同時変更によって破損する可能性がある複合データを操作する場合は、transaction 操作が用意されています。この操作には、2 つの引数(update 関数とオプションの完了コールバック)を与えます。uptate 関数はデータの現在の状態を引数として取り、書き込みたい新しい状態を返します。たとえば、特定のブログ投稿に対する賛成票の数を増分したい場合は、次のようなトランザクションを記述します。

Java
DatabaseReference upvotesRef = ref.child("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes");
upvotesRef.runTransaction(new Transaction.Handler() {
    @Override
    public Transaction.Result doTransaction(MutableData mutableData) {
        Integer currentValue = mutableData.getValue(Integer.class);
        if (currentValue == null) {
            mutableData.setValue(1);
        } else {
            mutableData.setValue(currentValue + 1);
        }

        return Transaction.success(mutableData);
    }

    @Override
    public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) {
       System.out.println("Transaction completed");
    }
});

Node.js
var upvotesRef = db.ref("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes");
upvotesRef.transaction(function (current_value) {
  return (current_value || 0) + 1;
});

デフォルト値が記述されていなかった場合はトランザクションを null で呼び出すことができるため、カウンタが null であったり、増分されていなかったりするかどうかを確認します。

上記のコードが transaction 関数なしで実行され、2 つのクライアントが同時に増分しようとした場合は、どちらも新しい値として 1 を書き込むため、2 ではなく 1 増分されることになります。

ネットワーク接続とオフラインでの書き込み

すべての Firebase クライアントはあらゆるアクティブ データの独自の内部バージョンを維持しています。データが書き込まれると、まず、このローカル バージョンに書き込まれます。次に、クライアントは、「ベスト エフォート」ベースでそのデータをデータベースや他のクライアントと同期します。

その結果、データベースへの書き込みはすべて、直ちにローカル イベントをトリガーします(これは、サーバーにデータが書き込まれる前の段階です)。つまり、Firebase を使用してアプリケーションを記述する場合は、ネットワークのレイテンシやインターネット接続に関係なく、アプリは応答性の高い状態を維持します。

接続が再確立されると、適切なイベントセットを受け取るため、クライアントが現在のサーバー状態に「追い付き」ます。この処理のためにカスタムコードを記述する必要はありません。

データのセキュリティ保護

Firebase Realtime Database には、データのさまざまなノードへの読み取りと書き込みのアクセス権を持つユーザーを定義できるセキュリティ言語が備わっています。詳細については、データのセキュリティ保護をご覧ください。

フィードバックを送信...