ウェブでのデータの読み取りと書き込み

(省略可)Firebase Local Emulator Suite でプロトタイピングおよびテストを行う

アプリが Realtime Database との間でどのようにデータを読み取り / 書き込みするのかを説明する前に、Realtime Database の機能のプロトタイピングとテストに使用できるツールである Firebase Local Emulator Suite について紹介します。異なるデータモデルの試行や、セキュリティ ルールの最適化、あるいはバックエンドとのやり取りで費用対効果の高い方法の検出を行う場合は、ライブサービスをデプロイせずにローカルで作業できるようにすると、大きなメリットが得られます。

Realtime Database エミュレータは Local Emulator Suite の一部であり、これを使用すると、アプリはエミュレートしたデータベースのコンテンツや構成とやり取りできるほか、エミュレートしたプロジェクトのリソース(関数、他のデータベース、セキュリティ ルール)とも任意でやり取りできます。

Realtime Database エミュレータを使用するには、いくつかの手順を実施するだけです。

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

Realtime Database と Cloud Functions については、詳しいチュートリアルをご覧ください。Local Emulator Suite の概要もご覧ください。

データベース参照を取得する

データベースでデータの読み書きを行うには、firebase.database.Reference のインスタンスが必要です。

ウェブ向けのモジュラー API

import { getDatabase } from "firebase/database";

const database = getDatabase();

ウェブ向けの名前空間付き API

var database = firebase.database();

データを書き込む

このドキュメントでは、データの取得に関する基本と、Firebase データの並べ替えとフィルタリングの方法について説明します。

Firebase データは、非同期リスナーを firebase.database.Reference にアタッチして取得します。リスナーはデータの初期状態で 1 回トリガーされます。さらに、データが変更されると、そのたびに再トリガーされます。

基本的な書き込みオペレーション

基本的な書き込みオペレーションには、set() を使用してデータを特定の参照に保存できます。そのパスにある既存のデータが置換されます。たとえば、ソーシャル ブログアプリでは、次のように set() でユーザーを追加できます。

ウェブ向けのモジュラー API

import { getDatabase, ref, set } from "firebase/database";

function writeUserData(userId, name, email, imageUrl) {
  const db = getDatabase();
  set(ref(db, 'users/' + userId), {
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

ウェブ向けの名前空間付き API

function writeUserData(userId, name, email, imageUrl) {
  firebase.database().ref('users/' + userId).set({
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

set() を使用すると、特定の場所にあるデータ(子ノードも含む)が上書きされます。

データを読み取る

value イベントのリッスン

パスにあるデータを読み取り、変更をリッスンするには、onValue() を使用してイベントを監視します。このイベントを使用して、特定のパスにあるコンテンツの静的スナップショットを、イベントの発生時に存在していたとおりに読み取ることができます。このメソッドはリスナーがアタッチされたときに 1 回トリガーされます。さらに、データ(子も含む)が変更されると、そのたびに再びトリガーされます。イベントのコールバックには、その場所にあるすべてのデータ(子のデータも含む)を含んでいるスナップショットが渡されます。データが存在しない場合、スナップショットから返されるのは、exists() を呼び出した場合は falseval() を呼び出した場合は null です。

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

ウェブ向けのモジュラー API

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const starCountRef = ref(db, 'posts/' + postId + '/starCount');
onValue(starCountRef, (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

ウェブ向けの名前空間付き API

var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

リスナーは snapshot を受信します。これには、イベントのときにデータベース内の指定された場所にあったデータが含まれています。snapshot のデータは val() メソッドを使用して取得できます。

データの 1 回読み取り

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

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

通常は、データを読み取り、バックエンドからデータの更新に関する通知を受け取るには、上記の値イベント手法を使用する必要があります。リスナーの手法は、使用量と課金を削減し、オンラインとオフラインのどちらでも最高のユーザー エクスペリエンスを実現できるよう最適化されています。

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

get() を必要以上に使用すると、帯域幅の使用が増加し、パフォーマンスの低下を招くおそれがあります。ただし、上記のリアルタイム リスナーを使用することで、これを回避できます。

ウェブ向けのモジュラー API

import { getDatabase, ref, child, get } from "firebase/database";

const dbRef = ref(getDatabase());
get(child(dbRef, `users/${userId}`)).then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

ウェブ向けの名前空間付き API

const dbRef = firebase.database().ref();
dbRef.child("users").child(userId).get().then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

オブザーバーを使用してデータを 1 回読み取る

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

これは 1 回読み込む必要があるだけで頻繁な変更やアクティブなリッスンを行うことは想定していないデータに対して有用です。たとえば、前の例にあるブログアプリでは、このメソッドを使用して、ユーザーが新しい投稿を作成し始めたときにユーザーのプロフィールを読み込んでいます。

ウェブ向けのモジュラー API

import { getDatabase, ref, onValue } from "firebase/database";
import { getAuth } from "firebase/auth";

const db = getDatabase();
const auth = getAuth();

const userId = auth.currentUser.uid;
return onValue(ref(db, '/users/' + userId), (snapshot) => {
  const username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
}, {
  onlyOnce: true
});

ウェブ向けの名前空間付き API

var userId = firebase.auth().currentUser.uid;
return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => {
  var username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
});

データの更新または削除

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

他の子ノードを上書きすることなく、ノードの特定の複数の子に同時に書き込むには、update() メソッドを使用します。

update() の呼び出し時に、キーのパスを指定して下位レベルの子の値を更新できます。スケーラビリティを向上させるためにデータが複数の場所に保存されている場合、データのファンアウトを使用してそのデータのすべてのインスタンスを更新できます。

たとえば、次のようなコードを使用して、ソーシャル ブログアプリで、投稿を作成し、その投稿で最近のアクティビティのフィードと投稿ユーザーのアクティビティのフィードを同時に更新するとします。

ウェブ向けのモジュラー API

import { getDatabase, ref, child, push, update } from "firebase/database";

function writeNewPost(uid, username, picture, title, body) {
  const db = getDatabase();

  // A post entry.
  const postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  const newPostKey = push(child(ref(db), 'posts')).key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  const updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return update(ref(db), updates);
}

ウェブ向けの名前空間付き API

function writeNewPost(uid, username, picture, title, body) {
  // A post entry.
  var postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  var newPostKey = firebase.database().ref().child('posts').push().key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  var updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return firebase.database().ref().update(updates);
}

この例では、push() を使用して、/posts/$postid にある全ユーザーの投稿が格納されているノード内に投稿を作成すると同時に、キーを取得しています。その後、このキーを使用して、/user-posts/$userid/$postid にあるユーザーの投稿に別のエントリを作成できます。

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

完了コールバックの追加

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

ウェブ向けのモジュラー API

import { getDatabase, ref, set } from "firebase/database";

const db = getDatabase();
set(ref(db, 'users/' + userId), {
  username: name,
  email: email,
  profile_picture : imageUrl
})
.then(() => {
  // Data saved successfully!
})
.catch((error) => {
  // The write failed...
});

ウェブ向けの名前空間付き API

firebase.database().ref('users/' + userId).set({
  username: name,
  email: email,
  profile_picture : imageUrl
}, (error) => {
  if (error) {
    // The write failed...
  } else {
    // Data saved successfully!
  }
});

データの削除

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

また、他の書き込みオペレーション(set()update() など)の値として null を指定する方法でも削除できます。この方法と update() を併用すると、API を 1 回呼び出すだけで複数の子を削除できます。

Promise の受信

Firebase Realtime Database サーバーにデータが commit されたときにそれを知るには、Promise を使用します。set()update() のどちらも、書き込みがデータベースに commit されたタイミングの把握に使用できる Promise を返すことができます。

リスナーのデタッチ

コールバックを削除するには、Firebase データベース参照で off() メソッドを呼び出します。

単一のリスナーは、パラメータとしてリスナーを off() に渡すと削除できます。off() を引数なしで呼び出すと、その場所にあるすべてのリスナーが削除されます。

親リスナーの off() を呼び出しても、その子ノードに登録されているリスナーが自動的に削除されるわけではありません。また、コールバックを削除するために子リスナーの off() も呼び出す必要があります。

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

増分カウンタなど、同時変更によって破損する可能性があるデータを操作する場合は、トランザクション オペレーションを使用します。このオペレーションには、update 関数とオプションの完了コールバックを与えることができます。update 関数はデータの現在の状態を引数として取り、書き込みたい新しい状態を返します。新しい値が正常に書き込まれる前に別のクライアントがその場所に書き込んだ場合、update 関数が現在の新しい値で再度呼び出されて、書き込みが再試行されます。

たとえば、このソーシャル ブログアプリの例では、次のようにして、投稿にスターを付ける操作と投稿のスターを取り消す操作をユーザーに許可し、投稿で得られたスターの数を追跡できます。

ウェブ向けのモジュラー API

import { getDatabase, ref, runTransaction } from "firebase/database";

function toggleStar(uid) {
  const db = getDatabase();
  const postRef = ref(db, '/posts/foo-bar-123');

  runTransaction(postRef, (post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

ウェブ向けの名前空間付き API

function toggleStar(postRef, uid) {
  postRef.transaction((post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

トランザクションを使用することで、複数のユーザーが同じ投稿にスターを同時に付けた場合や、クライアントのデータが古くなった場合でも、スターの数が不正確になることを防ぎます。トランザクションが拒否された場合、サーバーは現在の値をクライアントに返します。クライアントは更新された値でトランザクションを再度実行します。トランザクションが受け入れられるか、トランザクションを中止するまでこの処理が繰り返されます。

サーバーサイドのアトミックなインクリメント

上のユースケースでは 2 つの値をデータベースに書き込みます。投稿にスターを付ける / スターを外すユーザーの ID と、インクリメントされたスターの数です。ユーザーが投稿にスターを付けていることがわかっている場合は、トランザクションではなくアトミックなインクリメント オペレーションを使用できます。

ウェブ向けのモジュラー API

function addStar(uid, key) {
  import { getDatabase, increment, ref, update } from "firebase/database";
  const dbRef = ref(getDatabase());

  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = increment(1);
  update(dbRef, updates);
}

ウェブ向けの名前空間付き API

function addStar(uid, key) {
  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  firebase.database().ref().update(updates);
}

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

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

オフラインでのデータ操作

クライアントでネットワーク接続が切断された場合でも、アプリは引き続き適切に機能します。

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

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

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

オフラインの動作については、オンライン機能とオフラインの機能の詳細で説明します。

次のステップ