データの保存

このドキュメントでは、Firebase Realtime Database にデータを書き込むための 4 つのメソッド(set、update、push、transaction サポート)について説明します。

データの保存方法

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

データの保存

基本的なデータベース書き込みオペレーションは、新しいデータを指定したデータベース参照に保存し、そのパスにある既存のデータを置き換えるオペレーションである set です。set を理解するために、簡単なブログアプリをビルドします。アプリのデータは次のデータベース参照に保存されます。

Java
final FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("server/saving-data/fireblog");
Node.js
// Import Admin SDK
const { getDatabase } = require('firebase-admin/database');

// Get a database reference to our blog
const db = getDatabase();
const ref = db.ref('server/saving-data/fireblog');
Python
# Import database module.
from firebase_admin import db

# Get a database reference to our blog.
ref = db.reference('server/saving-data/fireblog')
Go
// Create a database client from App.
client, err := app.Database(ctx)
if err != nil {
	log.Fatalln("Error initializing database client:", err)
}

// Get a database reference to our blog.
ref := client.NewRef("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 dateOfBirth, String fullName) {
    // ...
  }

  public User(String dateOfBirth, String fullName, String nickname) {
    // ...
  }

}

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

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

usersRef.setValueAsync(users);
Node.js
const 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'
  }
});
Python
users_ref = ref.child('users')
users_ref.set({
    'alanisawesome': {
        'date_of_birth': 'June 23, 1912',
        'full_name': 'Alan Turing'
    },
    'gracehop': {
        'date_of_birth': 'December 9, 1906',
        'full_name': 'Grace Hopper'
    }
})
Go
// User is a json-serializable type.
type User struct {
	DateOfBirth string `json:"date_of_birth,omitempty"`
	FullName    string `json:"full_name,omitempty"`
	Nickname    string `json:"nickname,omitempty"`
}

usersRef := ref.Child("users")
err := usersRef.Set(ctx, map[string]*User{
	"alanisawesome": {
		DateOfBirth: "June 23, 1912",
		FullName:    "Alan Turing",
	},
	"gracehop": {
		DateOfBirth: "December 9, 1906",
		FullName:    "Grace Hopper",
	},
})
if err != nil {
	log.Fatalln("Error setting value:", err)
}

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

Java
usersRef.child("alanisawesome").setValueAsync(new User("June 23, 1912", "Alan Turing"));
usersRef.child("gracehop").setValueAsync(new User("December 9, 1906", "Grace Hopper"));
Node.js
const usersRef = ref.child('users');
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'
});
Python
users_ref.child('alanisawesome').set({
    'date_of_birth': 'June 23, 1912',
    'full_name': 'Alan Turing'
})
users_ref.child('gracehop').set({
    'date_of_birth': 'December 9, 1906',
    'full_name': 'Grace Hopper'
})
Go
if err := usersRef.Child("alanisawesome").Set(ctx, &User{
	DateOfBirth: "June 23, 1912",
	FullName:    "Alan Turing",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

if err := usersRef.Child("gracehop").Set(ctx, &User{
	DateOfBirth: "December 9, 1906",
	FullName:    "Grace Hopper",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

上記の 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<>();
hopperUpdates.put("nickname", "Amazing Grace");

hopperRef.updateChildrenAsync(hopperUpdates);
Node.js
const usersRef = ref.child('users');
const hopperRef = usersRef.child('gracehop');
hopperRef.update({
  'nickname': 'Amazing Grace'
});
Python
hopper_ref = users_ref.child('gracehop')
hopper_ref.update({
    'nickname': 'Amazing Grace'
})
Go
hopperRef := usersRef.Child("gracehop")
if err := hopperRef.Update(ctx, map[string]interface{}{
	"nickname": "Amazing Grace",
}); err != nil {
	log.Fatalln("Error updating child:", err)
}

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

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

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

usersRef.updateChildrenAsync(userUpdates);
Node.js
const usersRef = ref.child('users');
usersRef.update({
  'alanisawesome/nickname': 'Alan The Machine',
  'gracehop/nickname': 'Amazing Grace'
});
Python
users_ref.update({
    'alanisawesome/nickname': 'Alan The Machine',
    'gracehop/nickname': 'Amazing Grace'
})
Go
if err := usersRef.Update(ctx, map[string]interface{}{
	"alanisawesome/nickname": "Alan The Machine",
	"gracehop/nickname":      "Amazing Grace",
}); err != nil {
	log.Fatalln("Error updating children:", err)
}

この更新後は、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<>();
userNicknameUpdates.put("alanisawesome", new User(null, null, "Alan The Machine"));
userNicknameUpdates.put("gracehop", new User(null, null, "Amazing Grace"));

usersRef.updateChildrenAsync(userNicknameUpdates);
Node.js
const usersRef = ref.child('users');
usersRef.update({
  'alanisawesome': {
    'nickname': 'Alan The Machine'
  },
  'gracehop': {
    'nickname': 'Amazing Grace'
  }
});
Python
users_ref.update({
    'alanisawesome': {
        'nickname': 'Alan The Machine'
    },
    'gracehop': {
        'nickname': 'Amazing Grace'
    }
})
Go
if err := usersRef.Update(ctx, map[string]interface{}{
	"alanisawesome": &User{Nickname: "Alan The Machine"},
	"gracehop":      &User{Nickname: "Amazing Grace"},
}); err != nil {
	log.Fatalln("Error updating children:", err)
}

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

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

完了コールバックの追加

Node.js および Java Admin SDK では、データがいつ commit されたのかを把握したい場合に、完了コールバックを追加できます。これらの SDK の set メソッドと update メソッドはどちらも、完了コールバックをオプションとして取ります。このコールバックは、書き込みがデータベースに commit されたときに呼び出されます。なんらかの理由で呼び出しが失敗した場合は、失敗した理由を示すエラー オブジェクトがコールバックに渡されます。Python および Go Admin SDK では、書き込みメソッドはすべてブロック方式です。つまり、書き込みメソッドは、書き込みがデータベースに 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', (error) => {
  if (error) {
    console.log('Data could not be saved.' + error);
  } else {
    console.log('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.setValueAsync(new Post("gracehop", "Announcing COBOL, a New Programming Language"));

// We can also chain the two calls together
postsRef.push().setValueAsync(new Post("alanisawesome", "The Turing Machine"));
Node.js
const 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'
});
Python
posts_ref = ref.child('posts')

new_post_ref = posts_ref.push()
new_post_ref.set({
    'author': 'gracehop',
    'title': 'Announcing COBOL, a New Programming Language'
})

# We can also chain the two calls together
posts_ref.push().set({
    'author': 'alanisawesome',
    'title': 'The Turing Machine'
})
Go
// Post is a json-serializable type.
type Post struct {
	Author string `json:"author,omitempty"`
	Title  string `json:"title,omitempty"`
}

postsRef := ref.Child("posts")

newPostRef, err := postsRef.Push(ctx, nil)
if err != nil {
	log.Fatalln("Error pushing child node:", err)
}

if err := newPostRef.Set(ctx, &Post{
	Author: "gracehop",
	Title:  "Announcing COBOL, a New Programming Language",
}); err != nil {
	log.Fatalln("Error setting value:", err)
}

// We can also chain the two calls together
if _, err := postsRef.Push(ctx, &Post{
	Author: "alanisawesome",
	Title:  "The Turing Machine",
}); err != nil {
	log.Fatalln("Error pushing child node:", err)
}

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

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

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

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'
});;
Python
# This is equivalent to the calls to push().set(...) above
posts_ref.push({
    'author': 'gracehop',
    'title': 'Announcing COBOL, a New Programming Language'
})
Go
if _, err := postsRef.Push(ctx, &Post{
	Author: "gracehop",
	Title:  "Announcing COBOL, a New Programming Language",
}); err != nil {
	log.Fatalln("Error pushing child node:", err)
}

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()
const newPostRef = postsRef.push();

// Get the unique key generated by push()
const postId = newPostRef.key;
Python
# Generate a reference to a new location and add some data using push()
new_post_ref = posts_ref.push()

# Get the unique key generated by push()
post_id = new_post_ref.key
Go
// Generate a reference to a new location and add some data using Push()
newPostRef, err := postsRef.Push(ctx, nil)
if err != nil {
	log.Fatalln("Error pushing child node:", err)
}

// Get the unique key generated by Push()
postID := newPostRef.Key

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

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

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

増分カウンタなど、同時変更によって破損する可能性がある複合データを操作する場合のために transaction オペレーションが用意されています。

Java と Node.js では、transaction オペレーションに 2 つのコールバック(update 関数とオプションの完了コールバック)を与えます。Python と Go では、transaction はブロッキング オペレーションであるため、update 関数のみを受け入れます。

update 関数はデータの現在の状態を引数として取り、書き込みたい新しい状態を返します。たとえば、特定のブログ投稿に対する賛成票の数を増分したい場合は、次のようなトランザクションを記述します。

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
const upvotesRef = db.ref('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes');
upvotesRef.transaction((current_value) => {
  return (current_value || 0) + 1;
});
Python
def increment_votes(current_value):
    return current_value + 1 if current_value else 1

upvotes_ref = db.reference('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes')
try:
    new_vote_count = upvotes_ref.transaction(increment_votes)
    print('Transaction completed')
except db.TransactionAbortedError:
    print('Transaction failed to commit')
Go
fn := func(t db.TransactionNode) (interface{}, error) {
	var currentValue int
	if err := t.Unmarshal(&currentValue); err != nil {
		return nil, err
	}
	return currentValue + 1, nil
}

ref := client.NewRef("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes")
if err := ref.Transaction(ctx, fn); err != nil {
	log.Fatalln("Transaction failed to commit:", err)
}

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

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

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

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

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

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

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

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