データの読み取りと書き込み

(オプション)FirebaseEmulatorSuiteを使用したプロトタイプとテスト

アプリがRealtimeDatabaseの読み取りと書き込みを行う方法について説明する前に、RealtimeDatabaseの機能のプロトタイプ作成とテストに使用できる一連のツールであるFirebaseEmulatorSuiteを紹介しましょう。さまざまなデータモデルを試したり、セキュリティルールを最適化したり、バックエンドとやり取りするための最も費用効果の高い方法を見つけたりする場合は、ライブサービスを展開せずにローカルで作業できるようにすることをお勧めします。

RealtimeDatabaseエミュレーターはEmulatorSuiteの一部であり、これにより、アプリはエミュレートされたデータベースコンテンツと構成、およびオプションでエミュレートされたプロジェクトリソース(関数、その他のデータベース、セキュリティルール)と対話できます。emulator_suite_short

Realtime Databaseエミュレーターの使用には、いくつかの手順が含まれます。

  1. アプリのテスト構成にコード行を追加して、エミュレーターに接続します。
  2. ローカルプロジェクトディレクトリのルートから、 firebase emulators:startを実行します。
  3. 通常どおりRealtimeDatabaseプラットフォームSDKを使用するか、Realtime Database REST APIを使用して、アプリのプロトタイプコードから呼び出しを行います。

リアルタイムデータベースとクラウド機能に関する詳細なウォークスルーが利用可能です。また、 EmulatorSuiteの紹介もご覧ください。

DatabaseReferenceを取得する

データベースからデータを読み書きするには、 DatabaseReferenceのインスタンスが必要です。

DatabaseReference ref = FirebaseDatabase.instance.ref();

データを書き込む

このドキュメントでは、Firebaseデータの読み取りと書き込みの基本について説明します。

FirebaseデータはDatabaseReferenceに書き込まれ、参照によって発行されたイベントを待機またはリッスンすることで取得されます。イベントは、データの初期状態に対して1回発行され、データが変更されるたびに再度発行されます。

基本的な書き込み操作

基本的な書き込み操作では、 set()を使用してデータを指定された参照に保存し、そのパスにある既存のデータを置き換えることができます。次のタイプへの参照を設定できます: StringbooleanintdoubleMapList

たとえば、次のようにset()を使用してユーザーを追加できます。

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

await ref.set({
  "name": "John",
  "age": 18,
  "address": {
    "line1": "100 Mountain View"
  }
});

このようにset()を使用すると、子ノードを含む、指定された場所のデータが上書きされます。ただし、オブジェクト全体を書き直さなくても、子を更新することはできます。ユーザーがプロファイルを更新できるようにする場合は、次のようにユーザー名を更新できます。

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

// Only update the name, leave the age and address!
await ref.update({
  "age": 19,
});

update()メソッドはノードへのサブパスを受け入れ、データベース上の複数のノードを一度に更新できるようにします。

DatabaseReference ref = FirebaseDatabase.instance.ref("users");

await ref.update({
  "123/age": 19,
  "123/address/line1": "1 Mountain View",
});

データの読み取り

バリューイベントをリッスンしてデータを読み取る

パスでデータを読み取り、変更をリッスンするには、DatabaseReferenceのonValueプロパティを使用してDatabaseReferenceをリッスンしDatabaseEvent

DatabaseEventを使用して、イベント時に存在する特定のパスのデータを読み取ることができます。このイベントは、リスナーが接続されたときに1回トリガーされ、子を含むデータが変更されるたびにトリガーされます。イベントには、子データを含む、その場所のすべてのデータを含むsnapshotプロパティがあります。データがない場合、スナップショットのexistsプロパティはfalseになり、そのvalueプロパティはnullになります。

次の例は、データベースから投稿の詳細を取得するソーシャルブログアプリケーションを示しています。

DatabaseReference starCountRef =
        FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
    final data = event.snapshot.value;
    updateStarCount(data);
});

リスナーは、イベント時にデータベース内の指定された場所にあるデータをそのvalueプロパティに含むDataSnapshotを受け取ります。

データを1回読み取る

get()を使用して1回読み取る

SDKは、アプリがオンラインかオフラインかに関係なく、データベースサーバーとのやり取りを管理するように設計されています。

通常、バックエンドからデータの更新の通知を受け取るためにデータを読み取るには、上記のバリューイベント手法を使用する必要があります。これらの手法は、使用量と請求額を削減し、ユーザーがオンラインおよびオフラインで最高のエクスペリエンスを提供できるように最適化されています。

データが1回だけ必要な場合は、 get()を使用してデータベースからデータのスナップショットを取得できます。何らかの理由でget()がサーバー値を返すことができない場合、クライアントはローカルストレージキャッシュをプローブし、値がまだ見つからない場合はエラーを返します。

次の例は、データベースからユーザーの公開ユーザー名を1回取得する方法を示しています。

final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
    print(snapshot.value);
} else {
    print('No data available.');
}

get()を不必要に使用すると、帯域幅の使用が増加し、パフォーマンスが低下する可能性があります。これは、上記のようにリアルタイムリスナーを使用することで防ぐことができます。

once()でデータを1回読み取る

場合によっては、サーバーで更新された値をチェックするのではなく、ローカルキャッシュからの値をすぐに返す必要があります。そのような場合は、 once()を使用して、ローカルディスクキャッシュからデータをすぐに取得できます。

これは、一度だけロードする必要があり、頻繁に変更されることやアクティブなリスニングを必要としないデータに役立ちます。たとえば、前の例のブログアプリは、ユーザーが新しい投稿の作成を開始するときに、このメソッドを使用してユーザーのプロファイルを読み込みます。

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

データの更新または削除

特定のフィールドを更新する

他の子ノードを上書きせずにノードの特定の子に同時に書き込むには、 update()メソッドを使用します。

update()を呼び出すときに、キーのパスを指定することで、下位レベルの子の値を更新できます。データを複数の場所に保存してスケーリングを改善する場合は、データファンアウトを使用してそのデータのすべてのインスタンスを更新できます。たとえば、ソーシャルブログアプリは、投稿を作成し、それを最近のアクティビティフィードと投稿ユーザーのアクティビティフィードに同時に更新したい場合があります。これを行うために、ブログアプリケーションは次のようなコードを使用します。

void writeNewPost(String uid, String username, String picture, String title,
        String body) async {
    // A post entry.
    final postData = {
        'author': username,
        'uid': uid,
        'body': body,
        'title': title,
        'starCount': 0,
        'authorPic': picture,
    };

    // Get a key for a new Post.
    final newPostKey =
        FirebaseDatabase.instance.ref().child('posts').push().key;

    // Write the new post's data simultaneously in the posts list and the
    // user's post list.
    final Map<String, Map> updates = {};
    updates['/posts/$newPostKey'] = postData;
    updates['/user-posts/$uid/$newPostKey'] = postData;

    return FirebaseDatabase.instance.ref().update(updates);
}

この例では、 push()を使用して、 /posts/$postidにあるすべてのユーザーの投稿を含む投稿をノードに作成し、同時にキーとkeyを取得します。次に、このキーを使用して、 /user-posts/$userid/$postidにあるユーザーの投稿に2番目のエントリを作成できます。

これらのパスを使用すると、この例で両方の場所に新しい投稿を作成する方法など、 update()を1回呼び出すだけで、JSONツリー内の複数の場所に対して同時に更新を実行できます。この方法で行われる同時更新はアトミックです。すべての更新が成功するか、すべての更新が失敗します。

完了コールバックを追加する

データがいつコミットされたかを知りたい場合は、完了コールバックを登録できます。 set()update() )はどちらもFutureを返します。これには、書き込みがデータベースにコミットされたとき、および呼び出しが失敗したときに呼び出される成功コールバックとエラーコールバックをアタッチできます。

FirebaseDatabase.instance
    .ref('users/$userId/email')
    .set(emailAddress)
    .then((_) {
        // Data saved successfully!
    })
    .catchError((error) {
        // The write failed...
    });

データを削除する

データを削除する最も簡単な方法は、そのデータの場所への参照に対してremove()を呼び出すことです。

set()update() )などの別の書き込み操作の値としてnullを指定して、削除することもできます。この手法をupdate()とともに使用して、1回のAPI呼び出しで複数の子を削除できます。

データをトランザクションとして保存

インクリメンタルカウンターなどの同時変更によって破損する可能性のあるデータを操作する場合は、トランザクションハンドラーをrunTransaction()に渡すことでトランザクションを使用できます。トランザクションハンドラーは、データの現在の状態を引数として受け取り、書き込みたい新しい目的の状態を返します。新しい値が正常に書き込まれる前に別のクライアントがその場所に書き込むと、新しい現在の値を使用して更新関数が再度呼び出され、書き込みが再試行されます。

たとえば、ソーシャルブログアプリの例では、ユーザーが投稿にスタ​​ーを付けたりスターを外したりして、投稿が受け取ったスターの数を次のように追跡できるようにすることができます。

void toggleStar(String uid) async {
  DatabaseReference postRef =
      FirebaseDatabase.instance.ref("posts/foo-bar-123");

  TransactionResult result = await postRef.runTransaction((Object? post) {
    // Ensure a post at the ref exists.
    if (post == null) {
      return Transaction.abort();
    }

    Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
    if (_post["stars"] is Map && _post["stars"][uid] != null) {
      _post["starCount"] = (_post["starCount"] ?? 1) - 1;
      _post["stars"][uid] = null;
    } else {
      _post["starCount"] = (_post["starCount"] ?? 0) + 1;
      if (!_post.containsKey("stars")) {
        _post["stars"] = {};
      }
      _post["stars"][uid] = true;
    }

    // Return the new data.
    return Transaction.success(_post);
  });
}

デフォルトでは、トランザクション更新関数が実行されるたびにイベントが発生するため、関数runを複数回実行すると、中間状態が表示される場合があります。 applyLocallyfalseに設定して、これらの中間状態を抑制し、代わりにトランザクションが完了するまで待ってからイベントを発生させることができます。

await ref.runTransaction((Object? post) {
  // ...
}, applyLocally: false);

トランザクションの結果はTransactionResultであり、トランザクションがコミットされたかどうかや新しいスナップショットなどの情報が含まれています。

DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");

TransactionResult result = await ref.runTransaction((Object? post) {
  // ...
});

print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot

取引のキャンセル

トランザクションを安全にキャンセルする場合は、 Transaction.abort()を呼び出してAbortTransactionExceptionをスローします。

TransactionResult result = await ref.runTransaction((Object? user) {
  if (user !== null) {
    return Transaction.abort();
  }

  // ...
});

print(result.committed); // false

アトミックサーバー側の増分

上記のユースケースでは、データベースに2つの値を書き込んでいます。投稿にスタ​​ーを付けたりスターを外したりするユーザーのIDと、増分されたスター数です。ユーザーが投稿にスタ​​ーを付けていることがすでにわかっている場合は、トランザクションの代わりにアトミックインクリメント操作を使用できます。

void addStar(uid, key) async {
  Map<String, Object?> updates = {};
  updates["posts/$key/stars/$uid"] = true;
  updates["posts/$key/starCount"] = ServerValue.increment(1);
  updates["user-posts/$key/stars/$uid"] = true;
  updates["user-posts/$key/starCount"] = ServerValue.increment(1);
  return FirebaseDatabase.instance.ref().update(updates);
}

このコードはトランザクション操作を使用しないため、競合する更新がある場合に自動的に再実行されることはありません。ただし、インクリメント操作はデータベースサーバー上で直接行われるため、競合が発生する可能性はありません。

ユーザーが以前にスターを付けた投稿にスタ​​ーを付けるなど、アプリケーション固有の競合を検出して拒否する場合は、そのユースケースのカスタムセキュリティルールを作成する必要があります。

オフラインでデータを操作する

クライアントがネットワーク接続を失った場合でも、アプリは正常に機能し続けます。

Firebaseデータベースに接続されているすべてのクライアントは、アクティブなデータの独自の内部バージョンを維持しています。データが書き込まれるとき、最初にこのローカルバージョンに書き込まれます。次に、Firebaseクライアントは、そのデータをリモートデータベースサーバーおよび他のクライアントと「ベストエフォート」ベースで同期します。

その結果、データベースへのすべての書き込みは、データがサーバーに書き込まれる直前にローカルイベントをトリガーします。これは、ネットワークの遅延や接続に関係なく、アプリの応答性が維持されることを意味します。

接続が再確立されると、アプリは適切な一連のイベントを受信するため、カスタムコードを記述しなくても、クライアントは現在のサーバーの状態と同期します。

オフライン動作の詳細については、「オンラインおよびオフライン機能の詳細」を参照してください。

次のステップ