5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FirebaseRealtimeDatabaseとCloudFirestoreにおけるトランザクション処理

Last updated at Posted at 2019-12-07

福岡から世界中の"むずかしい"を簡単にする株式会社diffeasyCTOの西@_takeshi_24です。

この記事はアドベントカレンダー「diffeasyCTO西の24(にし)日連続投稿チャレンジ Advent Calendar 2019」の8日目の記事です。

この記事は「Nuxt.jsとFirebaseとCloudFunctionsでWebアプリ開発」シリーズとして、連載していきます。
Nuxt.jsとFirebaseなどを使ってWebアプリケーション開発にチャレンジしたい方、是非Qiitaアカウントかtwitterをフォローしていただき、ツッコミやいいね!お願いします!

##RealtimeDatabaseとCloudFirestore
FirebaseのNoSQLデータベース、「RealtimeDatabase」と「CloudFirestore」については以前「Nuxt.jsとFirebase RealtimeDatabase CloudFirestore でBaaS開発入門」で投稿していますので、そちらをご参照ください。

NoSQLではRDBのようにテーブルの結合ができません。
正規化せずに、データを保持するような設計も必要です。

例えば、社員テーブル(users)と部署テーブル(groups)が1対多で関係するデータがあるとします。RDBでは以下のような構成になります。
スクリーンショット 2019-12-07 18.43.08.png

このデータに対して、部署名と社員名の一覧を表示する場合、以下のようなSQLで取得できます。

select groups.name, users.name 
from users left join groups 
on users.group_id = groups.id;

これをNoSQLで設計する場合、以下のような形式でデータを正規化せずに保持します。
部署名と社員名を1つの取得処理で取得できるようにします。

users
users: {
  xxxxxxxxxxx: {
    name: "テスト太郎",
    groupId: "aaaaaaaaaa"
    groupName: "システム部"
  },
  yyyyyyyyyyy: {
    name: "テスト二郎",
    groupId: "bbbbbbbbbb"
    groupName: "経理部"
  },
  zzzzzzzzzz: {
    name: "テスト花子",
    groupId: "aaaaaaaaaa"
    groupName: "システム部"
  }
}
groups
groups: {
  aaaaaaaaaa: {
    name: "システム部"
  },
  bbbbbbbbbb: {
    name: "経理部"
  }
}

ただ、このようにデータを保持する場合、「経理部」の名称を「総務部」に変更する場合、社員情報(users)の部署名(group_name)も変更が必要です。
もし途中で更新処理が失敗してしまうと、一部のユーザーが「経理部」のまま残ってしまいます。

そこで、Firebaseのデータベースでもトランザクションを利用する必要があります。

##Firestoreのトランザクション処理
Firestoreのトランザクションには「トランザクション」と「バッチ書き込み」という2つの処理があります。
RDBで言うところの「トランザクション」はFirestoreでは「バッチ書き込み」と呼ばれます。

###Firestoreのバッチ書き込み
上記のデータの例で、部署コレクションの部署名を更新した場合に、所属する社員コレクションの部署名を更新するバッチ書き込み処理は以下のようになります。

async updateGroup(index) {
      // バッチ処理開始
      const batch = db.batch();
      // 部署の名称を更新
      const groupRef = db.collection("groups").doc(this.groups[index].groupId);
      await batch.set(
        groupRef,
        { name: this.groups[index].data.name },
        { merge: true }
      );
      //usersコレクションの部署名を更新
      await db
        .collection("users")
        .where("groupId", "==", this.groups[index].groupId)
        .get()
        .then(async snapshot => {
          await snapshot.forEach(async doc => {
            await batch.set(
              doc.ref,
              {
                groupId: this.groups[index].groupId,
                groupName: this.groups[index].data.name
              },
              { merge: true }
            );
          });
        });
      // バッチコミット
      batch.commit();
    }

これで、もし社員コレクションの部署名の更新に失敗すると、部署コレクションの部署名の更新もロールバックされ、データの一貫性が担保されます。

###Firestoreのトランザクション
Firestoreの「トランザクション」とは、同じデータを同時に更新して、データの整合性が合わなくなることを防ぐための処理です。

例えば、ポイント数を持つデータがあり、ポイントをカウントアップする際、「1ポイント」だったデータに2人が同時にポイントを付与した時のことを考えてみます。
本来1+1+1で3ポイントになるのが正しい処理になります。
しかし、同時に書き込みが走ってしまうと、1+1の処理と、1+1の処理になり、2ポイントになってしまいます。

これを防ぐための処理がトランザクションです。

    async pointUp(index) {
      //ユーザーデータを取得
      const userRef = await db.collection("users").doc(this.users[index].uid);
      //トランザクション開始
      db.runTransaction(t => {
        return t.get(userRef).then(doc => {
          const newPoint = doc.data().point ? doc.data().point + 1 : 1;
          //トランザクション更新
          t.update(userRef, { point: newPoint });
        });
      }).then(result => {
        this.getUsers();
      });
    }

##RealtimeDatabaseにおけるトランザクション処理
RealtimeDatabaseでもFirestoreの「トランザクション」と同じように1つのデータに対する同時書き込みを制御することはできます。

しかし、残念ながら、RealtimeDatabaseでは複数テーブルへの書き込みに対する一貫性を担保する、Firestoreの「バッチ処理」と同じ機能はありません。
これはそれぞれのデータベースの用途による違いだと思われます。

RealtimeDatabaseにおけるトランザクションも、Firestoreのトランザクションと同様です。

    async like(index, key) {
      //対象のデータを取得する
      const messageLikeRef = await firebase
        .database()
        .ref(`messages/${key}/like`);
      //newLikeCountに更新後の値が入る
      const newLikeCount = await messageLikeRef.transaction(function(
        currentValue
      ) {
        return (currentValue || 0) + 1;
      });
      //like数を更新
      this.messages[index].value.like = newLikeCount.snapshot;
    }

##最後に
FirebaseのFirestore、RealtimeDatabaseではデータを非正規化して、1回の処理で取得できるような設計にします。
今回の記事のように、バッチ書き込み、トランザクションを使って、データの書き込み時にデータの整合性を取る必要があります。

この記事は「Nuxt.jsとFirebaseとCloudFunctionsでWebアプリ開発」シリーズとして、連載していきます。
続きはアドベントカレンダー「diffeasyCTO西の24(にし)日連続投稿チャレンジ Advent Calendar 2019」に掲載していきます。

Nuxt.jsとFirebaseなどを使ってWebアプリケーション開発にチャレンジしたい方、是非Qiitaアカウントかtwitterをフォローしていただき、ツッコミやいいね!お願いします!

#advent_24のハッシュタグでフィードバックいただけると嬉しいです!

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?