質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Firebase

Firebaseは、Googleが提供するBasSサービスの一つ。リアルタイム通知可能、並びにアクセス制御ができるオブジェクトデータベース機能を備えます。さらに認証機能、アプリケーションのログ解析機能などの利用も可能です。

Cloud Firestore

Cloud Firestore は、自動スケーリングと高性能を実現し、アプリケーション開発を簡素化するように構築された NoSQLドキュメントデータベースです。

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

Q&A

解決済

1回答

1630閲覧

FirebaseFireStoreでの処理を非同期から同期にしたい

W.Taka

総合スコア31

Firebase

Firebaseは、Googleが提供するBasSサービスの一つ。リアルタイム通知可能、並びにアクセス制御ができるオブジェクトデータベース機能を備えます。さらに認証機能、アプリケーションのログ解析機能などの利用も可能です。

Cloud Firestore

Cloud Firestore は、自動スケーリングと高性能を実現し、アプリケーション開発を簡素化するように構築された NoSQLドキュメントデータベースです。

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

0グッド

0クリップ

投稿2021/04/04 09:03

編集2021/04/04 09:04

◆実現したいこと

ログインの処理にて、ログインユーザーがFirebaseFireStoreに登録されているかの判定を行いたい。

◆現在の実装

◇メインクラス
ログイン時に取得したUIDを元にユーザー情報を検索し、存在すればユーザー情報の更新し
存在しなければ新規登録を行う。

Swift

1 //ユーザー情報の登録を行う 2 if baseDto.uid != nil { 3 //ユーザー情報が既に存在する場合は更新、それ以外の場合は登録 4 if dbAccessModel.checkExistUserInfo(baseDto.uid) { 5 dbAccessModel.updateUserInfo(baseDto) 6 } else { 7 dbAccessModel.insertUserInfo(baseDto) 8 } 9 }

◇ユーザー情報存在チェッククラス
今回問題のクラス
渡されたUIDを元に検索をし、存在すればTrue、存在しなければFalseを返す。

Swift

1 /// ユーザー存在チェック 2 /// - Parameter uid: ユーザーID 3 /// - Returns: true:存在する false:存在しない 4 func checkExistUserInfo (_ uid: String) -> Bool { 5 let semaphore = DispatchSemaphore(value: 0) 6 var exist: Bool = false 7 8 db.collection("Users").whereField("user_id", isEqualTo: uid).getDocuments { (querySnapshot, err) in 9 if let err = err { 10 print("Error getting documents: (err)") 11 } else { 12 if querySnapshot!.documents.count > 0 { 13 exist = true 14 } 15 } 16 semaphore.signal() 17 } 18 19 semaphore.wait() 20 return exist 21 }

◆今回の問題点

ユーザーの存在チェックのあと、登録更新処理を行いたいのですが
FireStoreの使用上、DBへのアクセスが非同期になってしまいます。
このため判定の処理を待たずして判定が行われ、意図した動作をしない問題が出ていました。

上記の問題からユーザー情報取得処理の非同期処理を待つようにDispatchSemaphoreを実装しています。
ですがこの処理を実装したところ、semaphore.wait()のあと全く処理が返ってこず
先に処理が進まないという問題が発生している状況です。

色々と調べたところ、DispatchSemaphoreが最適解のようで、他ではこの実装でうまくいっているようなのですが
私の処理でうまくいかないのはどのようなことが原因なのでしょうか。

すみませんが、わかる方がいましたらご教授願います。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

結論から言うと、非同期処理を同期処理に変換することはできませんので、ちゃんと非同期として処理しましょう。

DispatchSemaphore を使ったコードが動かない理由は、Firebase のコールバックはメインスレッドで処理されますが、semaphore.wait() が先に実行されるので、これが終わらないとコールバックは実行されません。ところが、semaphore.wait() はコールバックで semaphore.signal() が実行されるのを待ってますので、デッドロックになり、永遠に終了しません。

また、(Firebase では不可能ですが) たとえコールバックをバックグラウンドで呼び出すことが可能だったとしても、処理を待つ間はメインスレッドを停止することになり、これは非常に良くないことです。

とりあえず今回のコードを非同期処理として書き直すと次のようになるでしょう。(なお、非同期処理の終了を待つ間に他の操作をされないような対策が必要になると思います。)

swift

1 //ユーザー情報の登録を行う 2 if baseDto.uid != nil { 3 //ユーザー情報が既に存在する場合は更新、それ以外の場合は登録 4 dbAccessModel.checkExistUserInfo(baseDto.uid) { exists in 5 if exists { 6 dbAccessModel.updateUserInfo(baseDto) 7 } else { 8 dbAccessModel.insertUserInfo(baseDto) 9 } 10 } 11 }

swift

1 func checkExistUserInfo(_ uid: String, callback: @escaping (Bool) -> Void) { 2 db.collection("Users").whereField("user_id", isEqualTo: uid).getDocuments { (querySnapshot, err) in 3 var exist: Bool = false 4 if let err = err { 5 print("Error getting documents: (err)") 6 } else { 7 if querySnapshot!.documents.count > 0 { 8 exist = true 9 } 10 } 11 callback(exist) 12 } 13 }

ところで、ユーザーの情報を持つドキュメントにアクセスするために user_id フィールドに uid を持たせてますが、ドキュメントの id を uid にすれば検索しなくても直接そのドキュメントにアクセスできるため、insertUserInfo と updateUserInfo を区別する必要がなくなります。(存在しないドキュメントに直接 setData できますし、merge: true を指定すれば既存のフィールドはそのままで新しい値だけ設定できます。)


しかしcheckExistUserInfoメソッドの動きについてかなり調べたのですが、
自分の理解力が乏しくどういう動きをしているのか理解ができませんでした・・・

まず、クロージャがどういうものか理解が曖昧なのでは。クロージャは次のように独立したメソッドとして書くこともきます。(クロージャをそれぞれ registerCallback, checkExistUserInfoCallback として切り出してみましたが、コンパイルを通してないので、文法的には曖昧です。また、メソッドに分けると checkExistUserInfo に渡す callback の受け渡し方法が問題になります…。)

swift

1class ViewController: UIViewController { 2 3 // 呼び出し順: A1 4 @IBAction func registerUser() { 5 //ユーザー情報の登録を行う 6 if baseDto.uid != nil { 7 dbAccessModel.checkExistUserInfo(baseDto.uid, callback: registerCallback) 8 } 9 } 10 11 // 呼び出し順: B4 12 func registerCallback(exists: Bool) { 13 //ユーザー情報が既に存在する場合は更新、それ以外の場合は登録 14 if exists { 15 dbAccessModel.updateUserInfo(baseDto) 16 } else { 17 dbAccessModel.insertUserInfo(baseDto) 18 } 19 } 20} 21 22class DBAccessModel { 23 // 仮に callback をプロパティとする。 24 private var callback: ((Bool) -> Void)? 25 26 // 呼び出し順: A2 27 func checkExistUserInfo(_ uid: String, callback: @escaping (Bool) -> Void) { 28 self.callback = callback 29 db.collection("Users") 30 .whereField("user_id", isEqualTo: uid) 31 .getDocuments(checkExistUserInfoCallback) 32 } 33 34 // 呼び出し順: B2 35 func checkExistUserInfoCallback(querySnapshot: QuerySnapshot?, err: Error?) { 36 var exist: Bool = false 37 if let err = err { 38 print("Error getting documents: (err)") 39 } else { 40 if querySnapshot!.documents.count > 0 { 41 exist = true 42 } 43 } 44 callback(exist) 45 } 46}

この処理の実行は、次の A と B の 2 つのフェーズで実行されます。

A1 登録ボタンを押すと registerUser が呼ばれる。
A2 registerUser から dbAccessModel.checkExistUserInfo を呼び出す。
A3 checkExistUserInfo は Firebase の db.collection 〜 .getDocuments を呼び出す。
A4 getDocuments は Firebase サーバーに問い合わせを開始し、すぐに return する。

ここでいったんボタンを押したイベントに対する処理は終了して、メインスレッドは別のイベント (画面の更新や、他のボタンを押したイベントなど) を処理できるようになります。そして、Firebase サーバーからお返事が返ってきたら、次の処理が行われます。

B1 Firebase サーバーからお返事が返る。
B2 getDocuments のコールバック checkExistUserInfoCallback が呼ばれる。
B3 checkExistUserInfoCallback では結果をチェックして exist の値をセットする。
B4 その exist を引数にして checkExistUserInfo のコールバック registerCallback を呼ぶ。

このように複数のフェーズに分かれて処理が行われるのが非同期処理の特徴です。そして、クロージャの部分は独立した関数として考える必要があり、かつ呼ばれるタイミングが違うことを理解する必要があります。


あ、mergeを指定すれば、なければInsert、あればupdateに自動的になるということでしょうか。

ドキュメントが存在しなければ merge の指定に関係なく insert になります。問題はドキュメントが存在する場合ですが、元のドキュメントが { id: 123, name: "abc", address: "def" } で、baseDto が { id: 123, name: "xyz" } の場合、merge を指定しない (または false を指定する) とドキュメントは上書きされるので address: "def" が失われてしまいますが、merge: true とした場合は baseDto に含まれないものはそのまま残ります。

投稿2021/04/04 22:17

編集2021/04/11 12:14
hoshi-takanori

総合スコア7901

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

W.Taka

2021/04/11 10:55

hoshi-takanoriさん 今回も遅れてしまいすみません。 本当に毎度お世話になっています。 とても助けられています。。。 記載していただいたソースで実行したところうまく動作しました! 本当にありがとうございます。 非同期・同期処理について再度基礎からしっかりと学習しなければと実感しました。。。 >処理を待つ間はメインスレッドを停止することになり、これは非常に良くないことです。 こちらについては再度調べていたところそういった記事を見つけました。 よくよく考えればSwiftに限らずよくないことですよね・・・ 回答していただいた内容について、チェック処理の呼び出しをクロージャにすることで チェック結果を得てから判定処理に入るようにした。ということは理解できました。 しかしcheckExistUserInfoメソッドの動きについてかなり調べたのですが、 自分の理解力が乏しくどういう動きをしているのか理解ができませんでした・・・ 自分の中では 1、dbAccessModel.checkExistUserInfo(baseDto.uid) が実行される 2、checkExistUserInfo内のdb.collection("Users").whereField("user_id", isEqualTo: uid).getDocumentsが非同期処理で実行される 3、2の処理が完了後、callbackが呼ばれ結果が返される 4、3で返されたあたいがexistsに入ってきて判定処理がされる このような流れの認識なのですがここは問題ないでしょうか。 またここで疑問なのが、checkExistUserInfoが戻り値を持っていないことです。 dbAccessModel.checkExistUserInfo(baseDto.uid) を実行しても戻り値を持っていないので どのようにして呼び出しもとのクロージャのexistsに返ってくるのでしょうか。
W.Taka

2021/04/11 10:57 編集

>ところで、ユーザーの情報を持つドキュメントにアクセスするために user_id フィールドに uid を持たせ>てますが、ドキュメントの id を uid にすれば検索しなくても直接そのドキュメントにアクセスできるた>め、insertUserInfo と updateUserInfo を区別する必要がなくなります。(存在しないドキュメントに直>接 setData できますし、merge: true を指定すれば既存のフィールドはそのままで新しい値だけ設定で>きます。) あ、mergeを指定すれば、なければInsert、あればupdateに自動的になるということでしょうか。
hoshi-takanori

2021/04/11 12:21

回答欄に追記しました。反応が遅いのはぜんぜん構いませんので、分からない点があればまたご質問ください。
W.Taka

2021/07/14 08:38

hoshi-takanoriさん 遅くなり大変申し訳ございません。 年度が変わってから仕事の方が忙しくなり、勉強する習慣が途切れてしまい こんなにも遅くなってしまいました。 最近落ち着いたので、改めてしっかりとクロージャと非同期について 調べてきました。 ご回答いただいたクロージャについてのご説明理解ができました。 Escapingの部分が初め中々理解できなかったのですが、 改めてしっかりと調べて学んだこともあり理解ができました。 丁寧にご説明いただき本当にありがとうございます。 最後に1つ確認をさせてください。 当初の質問の問題はデッドロックを引き起こしていたことが問題でしたが その問題の原因は、    checkExistUserInfoメソッドに入った時  ①どの処理よりも先にsemaphore.wait()がメインスレッドで実行され   semaphore.signal()の実行を待つ  ②getDocumentsもメインスレッドで実行されるが、   semaphore.wait()がメインスレッドで実行中のため待ち状態になる  ③getDocuments内にsemaphore.signal()があるため、   いつになってもsemaphore.wait()が解放されず、デッドロックを引き起こす。 このような理解であっていますでしょうか。
hoshi-takanori

2021/07/14 09:10

大まかな理解としてはそれでも良いのですが、厳密には「どの処理よりも先にsemaphore.wait()が〜」というのは抵抗がありますね…。 あくまで、先に実行されるのは getDocuments です。ただし、{ (querySnapshot, err) in 〜 } のクロージャは引数として渡されるだけで、まだ実行はされません。次に semaphore.wait() が実行されて待ち状態になりますが、これがなければ getDocuments の結果が返ってきた時にクロージャがメインスレッドで実行され、その中で semaphore.signal() が実行されて待ち状態が解除されるはずですが、そもそも semaphore.wait() でメインスレッドが止まってるのでクロージャは実行さず、デッドロックになります。
W.Taka

2021/07/14 09:38

ご返信ありがとうございます。 そういうことでしたか! どこかの記事でsemaphore.wait()が先に実行されるというのを 謝って捉えていたかもしれないです。 まだ理解が甘かったですね。 指摘していただきありがとうございます しっかりと理解したかった部分なので大変助けられました。 本当にありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問