【Firebase】アカウント削除の後始末にあたりトランザクションを対応した

研究
OLYMPUS PEN E-PL9 (c)Maya
この記事は約11分で読めます。

本日こそ最終回。次の課題へ掛かります。

firebaseチャット試作品 – maya.pg.

firebaseチャット試作品 – maya.pg.

アカウント削除時に
・全体チャットデータの削除
・firestoreに出したプロフィール画像他の参照可能データの削除
これらを対応しなくてはならず。
以下のように記述していたら、案の定うまくいかなかったのです。

// アカウント削除
document.querySelector("#remove_account_form").addEventListener("submit", function(event) {
  if(confirm('アカウントを削除します。本当によろしいですか?')){
    document.querySelector('#remove_account_form button').setAttribute('disabled', true);
    document.querySelector('#remove_account_form button').innerText = '削除中...';

    var user = firebase.auth().currentUser;
    var credential = firebase.auth.EmailAuthProvider.credential(
      user.email,
      getElementValue('remove_account_password')
    );

    user.reauthenticateWithCredential(credential).then(function() {
      // User re-authenticated.

      user.delete().then(function() {
        var db = firebase.firestore();
        // チャットデータを削除
        var openchatsRef = db.collection("openchats");
        var query = openchatsRef.where("uid", "==", user.uid);
        query.get()
        .then(function(querySnapshot) {
          querySnapshot.forEach(function(doc) {

            db.collection("openchats").doc(doc.id).delete().then(function() {
              console.log("Document successfully deleted!");
            }).catch(function(error) {
              console.error("Error removing document: ", error);
            });
            // // doc.data() is never undefined for query doc snapshots
            // console.log(doc.id, " => ", doc.data());
          });
        })
        .catch(function(error) {
          console.log("Error getting documents: ", error);
        });

        // プロフィールデータを削除
        db.collection("users").doc(user.uid).delete().then(function() {
          console.log("Document successfully deleted!");
        }).catch(function(error) {
          console.error("Error removing document: ", error);
        });

        alert('アカウントを削除しました。ご利用ありがとうございました。');
        firebase.auth().signOut();
      }).catch(function(error) {
        var errorCode = error.code;
        var errorMessage = error.message;
        alert(errorCode + ', ' + errorMessage);
      });

    }).catch(function(error) {
      var errorCode = error.code;
      var errorMessage = error.message;
      alert(errorCode + ', ' + errorMessage);
    });
  }

  event.preventDefault();
}, false);

全体チャットから削除する対象のdocumentを検索してきてforEachで削除するまでは正しかったのですが、そちらの処理が終わる前にsignOutが走ってしまいます。

これを解決する為にはトランザクションを使います。

トランザクションとバッチ書き込み | Firebase

Cloud Firestore は、データを読み書きするアトミック オペレーションをサポートしています。一連のアトミック オペレーションでは、すべてのオペレーションが正常に完了するか、またはどのオペレーションも適用されないかのいずれかです。Cloud Firestore には 2 種類のアトミック オペレーションがあります。 トランザクション: トランザクションは、1 つ以上のドキュメントに対して読み取り / 書き込みを行う一連のオペレーションです。 バッチ書き込み: バッチ書き込みは、1 つ以上のドキュメントに対して書き込みを行う一連のオペレーションです。 1 回のトランザクションまたはバッチ書き込みでは、最大 500 のドキュメントに書き込みを行うことができます。書き込みに関連するその他の制限については、 割り当てと上限 をご覧ください。 Cloud Firestore クライアント ライブラリを使用して、複数のオペレーションを 1 つのトランザクションにまとめることが可能です。フィールドの値を、その現行値またはその他のフィールドの値に基づいて更新する場合には、トランザクションが便利です。 トランザクションは、任意の数の get() オペレーションと、その後に続く任意の数の書き込みオペレーション( set()、 update()、 delete() など)で構成されます。同時編集の場合、Cloud Firestore はトランザクション全体を再実行します。たとえば、トランザクションがドキュメントを読み取り、別のクライアントがそれらのドキュメントを変更すると、Cloud Firestore はトランザクションを再試行します。この機能により、常に整合性のある最新データに対してトランザクションが実行されます。 トランザクションでは、書き込みが部分的に適用されることはありません。成功したトランザクションの完了時にすべての書き込みが実行されます。 トランザクションを使用する場合は、次の点に注意してください。 読み取りオペレーションは書き込みオペレーションの前に実行する必要があります。 トランザクションが読み取るドキュメントに対して同時編集が影響する場合は、トランザクションを呼び出す関数(トランザクション関数)が複数回実行されることがあります。 トランザクション関数はアプリケーションの状態を直接変更してはなりません。 クライアントがオフラインの場合、トランザクションは失敗します。 次の例は、トランザクションを作成して実行する方法を示します。 DocumentReference cityRef = db.Collection(“cities”).Document(“SF”); db.RunTransactionAsync(transaction => { return transaction.GetSnapshotAsync(cityRef).ContinueWith((snapshotTask) => { DocumentSnapshot snapshot = snapshotTask.Result; long newPopulation = snapshot.GetValue (“Population”) + 1; Dictionary updates = new Dictionary { { “Population”, newPopulation} }; transaction.Update(cityRef, updates); }); }); トランザクション関数内でアプリケーションのステータスを変更しないでください。トランザクション関数は複数回実行されることがあり、UI スレッドに対して実行される保証がないため、変更すると同時実行の問題が発生します。代わりに、トランザクション関数から必要な情報を渡します。次の例は、前の例に基づいて、トランザクションから情報を渡す方法を示します。 // This is not yet supported.

但し公式のサンプルはdocument idが指定されていますが、私の場合は検索してループさせないとなりませんでした。

// Create a reference to the SF doc.
var sfDocRef = db.collection("cities").doc("SF");

// Uncomment to initialize the doc.
// sfDocRef.set({ population: 0 });

return db.runTransaction(function(transaction) {
    // This code may get re-run multiple times if there are conflicts.
    return transaction.get(sfDocRef).then(function(sfDoc) {
        if (!sfDoc.exists) {
            throw "Document does not exist!";
        }

        // Add one person to the city population.
        // Note: this could be done without a transaction
        //       by updating the population using FieldValue.increment()
        var newPopulation = sfDoc.data().population + 1;
        transaction.update(sfDocRef, { population: newPopulation });
    });
}).then(function() {
    console.log("Transaction successfully committed!");
}).catch(function(error) {
    console.log("Transaction failed: ", error);
});

完遂した実装は以下の通り。
最初に22行目にもreturnをつけるのに気がつかず、37行目の公開プロフィール情報の削除の方が先に動いてしまい元の木阿弥でした。
22行目にreturnを付け、定義したtransactionが全て完了した段階でthenに飛ばすことができました。

// アカウント削除
document.querySelector("#remove_account_form").addEventListener("submit", function(event) {
  event.preventDefault();
  if(confirm('アカウントを削除します。本当によろしいですか?')){
    document.querySelector('#remove_account_form button').setAttribute('disabled', true);
    document.querySelector('#remove_account_form button').innerText = '削除中...';

    var user = firebase.auth().currentUser;
    var db = firebase.firestore();
    // チャットデータを削除
    // var openchatsRef = db.collection("openchats");

    var user = firebase.auth().currentUser;
    var credential = firebase.auth.EmailAuthProvider.credential(
      user.email,
      getElementValue('remove_account_password')
    );

    const openchatsRef = db.collection('openchats'); // .where("uid", "==", user.uid)
    const usersRef = db.collection('users').doc(user.uid);

    return db.runTransaction(function(transaction) {
      // 全体チャットデータの削除
      var query = openchatsRef.where("uid", "==", user.uid);
        query.get()
        .then(async function(querySnapshot) {
          querySnapshot.forEach(function(doc) {

            return transaction.get(openchatsRef.doc(doc.id)).then(function(sfDoc) {
              transaction.delete(openchatsRef.doc(doc.id));
            });
          });
          // console.log(querySnapshot.docs);
        });
      
	  // 公開プロフィール情報削除
      return transaction.get(usersRef).then(function(sfDoc) {
        transaction.delete(usersRef);
      });
    }).then(function() {

      // storageのプロフィール画像削除
      var storageRef = firebase.storage().refFromURL(user.photoURL);

      // Delete the file
      storageRef.delete().then(function() {
        // File deleted successfully
        console.log('File deleted successfully');

        user.reauthenticateWithCredential(credential).then(function() {
          user.delete().then(function() {
            alert('アカウントを削除しました。ご利用ありがとうございました。');
            firebase.auth().signOut();
            location.reload();
          });
        });
      }).catch(function(error) {
        // Uh-oh, an error occurred!
      });

    }).catch(function(error) {
      console.log(error.line);
      console.log("Transaction failed: ", error);
    });

  }

  event.preventDefault();
}, false);

加えて、43行目と46行目でしれっとプロフィール画像の処理を追加し、ひととおりやりたいことはやれたので、エキシビジョン的に動く状態で公開しておきます。
なおgithubはこちらに公開しています。

mayarin/firebase-sample

firebase hosting 上で javascriptを動作させ、以下の実装を行います。 ・firebase auth のアカウントを登録、管理、削除します。 ・firebase storage上に自身のアカウントのプロフィール画像を登録。 ・cloud firestore へデータの読み書きを行います。 ・サインアップ ・サインイン ・サインアウト ・パスワード再設定リンク請求 ・サインイン後プロフィール更新 ・ログインメールアドレス変更 ・ログインパスワード変更 ・アカウント削除 ・アカウント全体でのチャット ・個人間チャットを積む ・チャットメッセージが消された際のリフレッシュを行う ・アカウントが削除された際にチャットデータも削除 ・firebaseプロジェクト ・firebase authにてメールアドレス・パスワードでのログインを利用する旨登録 ・firebase storage ・cloud firestore https://maya-pg.net/

コメント

タイトルとURLをコピーしました