20210502のiOSに関する記事は7件です。

備忘録_スマホアプリ開発の現場に入る前に知っておくとよいこと。

はじめに 自分はWeb系のエンジニアです。 スマホアプリ開発経験は0です。 今回、Webの技術でスマホアプリを開発する現場に常駐することになったので、 自分で調べたことをまとめます。 自分のアウトプット用なのと、 同じ感じの人の助けになればと思います。 理解が間違っているところがあれば、ご指摘頂けると嬉しいです。 基礎知識 スマホのOSはAndroidとiOSがある。 日本ならiPhoneが多いけど、iPhoneだけ対応ってことは少ない。 Webと違いリリースは、サーバにデプロイして完了とはならない。 androidならPlayストア、iOSならApp Storeにアプリを公開する。 そのため、GoogleやAppleなりの審査がある。 つまるとこ、android用、iOS用で作らないといけない。 クロスプラットフォームな開発できるFWでも、プログラムは共通だけど、 リリースする際にAndroidとiOSで、それ用にビルドしてあげないといけない。 基本、Android StudioとXcodeは必要って認識で大丈夫だと思う。(現場によって異なるけど) Android スマートフォン ユーザがアプリをスマホに入れるなら基本的にPlayストアからアプリをインストールする。 Playストアからアプリを配布するにはGoogle Play Developerに登録が必要。 Playストアにアプリを公開するには? Google Play ConsoleというPlayストアへのAppの提出、Appの管理ができるサービスがある。 Google Play Consoleで以下を実施。 1. アプリ情報を登録 2. apkファイルをアップロード(※apkファイルではなくAndroid App Bundleを推奨) 3. レーティングを設定する 4. 価格と販売/配布地域を設定する 5. Appを審査へ提出 6. App ステータスの確認と審査で出た問題を解決する 7. 審査が承認されたらPlayストアに公開される。 ※Android App Bundleを推奨。 2021年8月より、Google Playでの新規アプリの公開はAndroid App Bundleで行う必要がある。 アプリのコンパイル済みコードとリソースがすべて含まれた公開形式。 APKの生成と署名はGoogle Playが行う。 Google PlayはApp Bundleから、デバイス設定毎に最適化したAPKを生成、配信する。 それにより、個々のデバイスでAppを実行するのに必要なコードとリソースだけがDLされる。 Developerは、多様なデバイスのサポートを最適化する為に複数のAPKをビルド、署名、管理する必要がない。 apkファイル? apk Android Application Packageの略。 ZIP形式でアーカイブファイルの一種。 拡張子は.apk 決まった手順でAndroid端末にアプリをインストールできるファイル。 ビルド後にできるファイルと思えば大丈夫。 作成方法(ビルド方法)はコマンドかAndroid Studioを利用。 僕がいる現場は、ビルドする時だけAndroid Studioを使っている。 コマンドでするにもAndroidのsdkが必要だから、Androi Studioはインストールした方が楽だった気がしますが、 もっと楽な方法はあるかもしれません。 iPhone ユーザがアプリをiPhoneに入れるならApp Storeからアプリをインストールする。 App Storeからアプリを配布するにはApple Developer Programに登録が必要。 App Storeにアプリを公開するには? App Store ConnetというApp StoreへのAppの提出、Appの管理ができるサービスがある。 App Store Connectで以下を実施。 1. 契約書に同意し、税金と銀行口座情報を入力する。 2. ユーザを追加して役割を割り当てる 3. Appを追加してビルドをアップロードする 4. 価格および配信状況を設定する 5. Appを審査に提出する。 6. App ステータスの確認と審査で出た問題を解決する 7. 審査が承認されたらApp Storeに公開される。 細かいところは、Appleの公式のガイドラインで確認してください。 少し混乱したのが、Appleの場合、Appをアップロードするのにアーカイブファイルを作成してアップロードするみたいな方法は一般的ではないみたい。 XcodeからGUIでビルドを実行して、そのままApp Store Connnectにアップロードできる。 アーカイブファイルを作成してアップロードする方法もあるようです。 開発フロー 企画 ↓ 要件 ↓ 設計・実装 ↓ ビルド ↓ テスト ↓ 審査 ↓ リリース 上記の流れで開発していく。 企画、要件、設計・実装の流れはWEBとさほど変わらない。 大きく異なるのはビルドとテスト。 自分はクロスプラットフォームな開発ができるFWを使って開発している現場だが、 ビルドをandroid用、iOS用でビルドする。 このビルドが大変、混乱した。 色々な方法があると思うが自分がいる現場は以下。 Android Android Studioでapkファイルを作成する。 adb(Android Debug Bridge)コマンドがかかれたbatで実機にインストールする。 iOS Xcodeでプロジェクトを開く。(FWでios用のプロジェクトは作成済み) Xcodeから実機にインストールする。 テスト 実機で動作確認を行う。 デバックは、USBでPCに接続して、下記で確認を行う。 android Chrome Inspector iOS Safari Inspector おわりに ざっくりと当たり前のことを書きましたが、OSによってビルド方法が異なる、審査っていう工程があるっていうのを認識して その周りを学習しないといけないです。 IonicやReact NativeとかはWebが分かるなら大丈夫だよーっていうのを信じたら痛い目にあいました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter】TextFormField, TextField onChanged使用時のカーソル位置が最初に戻ってしまう不具合対応

発生していた不具合 TextFormFieldに入力するたびにカーソルの位置が最初に戻ってしまう現象が発生していました。 原因 入力ごとに毎回初期値が変更されたとみなされ、カーソルの位置が初期状態に戻ってしまっていた。 修正方法 初期値入力時に、テキスト情報に併せて、offset情報も渡す。 コード例 controller: TextEditingController.fromValue( TextEditingValue( text: initialString ?? "", selection: TextSelection.collapsed(offset: initialString?.length ?? 0), ), ), 実際の修正例 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】VIPERを勉強してみたPart1

はじめに VIPERを勉強したので、基本的なことをまとめていこうと思います。 GitHub VIPERとは クリーンアーキテクチャーをiOS向けにしたシステムアーキテクチャのこと。 View, Interactor, Presenter, Entity, Routerの頭文字からとった。 システムアーキテクチャーとは、今までのGUIアーキテクチャー(MVC, MVVM, MVP)のように、Viewとその他みたいな考え方ではなく、画面遷移やAPI通信、データ保存などを考慮した設計のこと。 それぞれの役割 View: ViewとViewController Interactor: API通信担当 Presenter: 自分以外の中継役 Entity: データそのもの Router: 画面遷移担当 特徴 ・徹底的な疎結合 →Entity以外全てprotocolで繋ぐ ・Presenterは内部で状態をもたない →いつどのような入力に対しても必ず同じ出力になる。(Entityの違いはある) ・PresenterのメソッドはViewで起きたものに依存した名前にする →viewDidLoad, buttonDidTappedなど ・ViewとRouter以外はimport UIKitだめ、絶対 ・Interactorはデータを返すだけに徹する →API通信、端末内保存、メソッドで計算しただけなど関係なく、最後はデータを返すだけ。 他のモジュールからはどのようにデータを返したのかわからなくする。 データの返し方はRxSwift, protocol, closureなど、なんでもいい ・Entityに処理を書かない →純粋にデータを保持した型 処理の流れ 1.Routerdえ画面を生成し、DI(依存性注入)させる 2.生成された画面を表示 3.ViewからイベントをPresenterに知らせる →ライフサイクル、ボタンタップ... 4.PresenterはViewから送られてきたイベントの内容に合わせて以下のような処理をする ・Viewに対して画面の更新依頼する →Viewは依頼された通りに画面を更新する ・Interactorに対してデータの取得依頼をする →Interactorは依頼されたデータの取得が完了したらPresenterに通知する ・Routerに対して画面遷移の依頼をする →Routerは依頼された画面へ遷移する 命名 View ・画面の更新 ラベルの文字変更 UITableViewのreload など ・Presenterへのイベント通知担当 ライフサイクル ボタンのタップ、セルのタップ など GitHubSearchViewController import UIKit protocol GitHubSearchView: AnyObject { func initView() func startLoading() func finishLoading() func reloadTableView(items: [GitHubSearchEntity]) } final class GitHubSearchViewController: UIViewController { @IBOutlet private weak var textField: UITextField! @IBOutlet private weak var searchButton: UIButton! @IBOutlet private weak var indicator: UIActivityIndicatorView! @IBOutlet private weak var tableView: UITableView! // presenterへのアクセスはprotocolを介して行う private var presenter: GitHubSearchPresentation! override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self tableView.dataSource = self tableView.register(GitHubSearchTableViewCell.nib, forCellReuseIdentifier: GitHubSearchTableViewCell.identifier) searchButton.addTarget(self, action: #selector(searchButtonDidTapped), for: .touchUpInside) // presenterにイベントを通知 presenter.viewDidLoad() } func inject(presenter: GitHubSearchPresentation) { self.presenter = presenter } } // MARK: - @objc func @objc private extension GitHubSearchViewController { func searchButtonDidTapped() { // presenterにイベントを通知 presenter.searchButtonDidTapped(word: textField.text) } } // MARK: - GitHubSearchView extension GitHubSearchViewController: GitHubSearchView { func initView() { DispatchQueue.main.async { self.tableView.isHidden = true self.indicator.isHidden = true } } func startLoading() { DispatchQueue.main.async { self.tableView.isHidden = true self.indicator.isHidden = false } } func finishLoading() { DispatchQueue.main.async { self.tableView.isHidden = false self.indicator.isHidden = true } } func reloadTableView(items: [GitHubSearchEntity]) { DispatchQueue.main.async { self.tableView.reloadData() } } } // MARK: - UITableViewDelegate extension GitHubSearchViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) // presenterにイベントを通知 presenter.selectItem(indexPath: indexPath) } } // MARK: - UITableViewDataSource extension GitHubSearchViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // presenterにイベントを通知 return presenter.getSearchedItems().count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: GitHubSearchTableViewCell.identifier, for: indexPath) as! GitHubSearchTableViewCell // presenterにイベントを通知 let item = presenter.getSearchedItems()[indexPath.row] cell.configure(gitHubSearch: item) return cell } } Interactor ・ビジネスロジック担当(Utility) ・Presenterから依頼されたビジネスロジックを実装し、結果を返す。 delegate, closure, RxSwift... ・import UIKitだめ、絶対 UIを気にしない GitHubSearchInteractor import Foundation protocol GitHubSearchUsecase { func get(parameters: GitHubSearchParameters, handler: ResultHandler<[GitHubSearchEntity]>?) func getSearchedItems() -> [GitHubSearchEntity] } // 他のアーキテクチャーでいうUtilityの役割も持つ final class GitHubSearchInteractor { private var searchedItems: [GitHubSearchEntity] init() { searchedItems = [] } } // MARK: - GitHubSearchUsecase extension GitHubSearchInteractor: GitHubSearchUsecase { func get(parameters: GitHubSearchParameters, handler: ResultHandler<[GitHubSearch]>? = nil) { guard parameters.validation else { handler?(.failure(.error)) return } guard let url = URL(string: "https://api.github.com/search/repositories?\(parameters.queryParameter)") else { handler?(.failure(.invalidUrl)) return } let task = URLSession.shared.dataTask(with: url) { data, _, _ in guard let data = data, let gitHubResponse = try? JSONDecoder().decode(GitHubSearchEntityResponse.self, from: data), let items = gitHubResponse.items else { handler?(.failure(.error)) return } self.searchedItems = items handler?(.success(items)) } task.resume() } func getSearchedItems() -> [GitHubSearchEntity] { return searchedItems } } Presenter ・Viewから受け取ったイベントを元に別クラスに依頼 Viewに対して画面更新を依頼 Interactorに対してデータの取得を依頼 Routerに対して画面遷移を依頼 ・Presenterが提供するメソッド名はViewのメソッド名と同じ viewDidLoad, buttonDidTapped... ・Presenterに状態を持たせない ・import UIKitだめ、絶対 UIを気にしない GitHubSearchPresenter import Foundation protocol GitHubSearchPresentation: AnyObject { func viewDidLoad() func searchButtonDidTapped(word: String?) func selectItem(indexPath: IndexPath) func getSearchedItems() -> [GitHubSearchEntity] } // 他との部品以外はパラメータを持たない // 他との中継役にだけに徹する final class GitHubSearchPresenter { // view, interactor, routerへのアクセスはprotocolを介して行う // 循環参照しないようにviewだけweak private weak var view: GitHubSearchView? private var interactor: GitHubSearchUsecase private var router: GitHubSearchWireframe init(view: GitHubSearchView, interactor: GitHubSearchUsecase, router: GitHubSearchWireframe) { self.view = view self.interactor = interactor self.router = router } } // MARK: - GithubSearchPresentation extension GitHubSearchPresenter: GitHubSearchPresentation { func viewDidLoad() { view?.initView() } func searchButtonDidTapped(word: String?) { let parameters = GitHubSearchParameters(searchWord: word) view?.startLoading() interactor.get(parameters: parameters) { [weak self] result in guard let self = self else { return } self.view?.finishLoading() switch result { case .success(let items): self.view?.reloadTableView(items: items) case .failure(let error): self.router.showAlert(error: error) } } } func selectItem(indexPath: IndexPath) { let gitHubSearchEntity = interactor.getSearchedItems()[indexPath.row] let initParameters: WebUsecaseInitParameters = .init(entity: gitHubSearchEntity) router.showWeb(initParameters: initParameters) } func getSearchedItems() -> [GitHubSearchEntity] { return interactor.getSearchedItems() } } Entity ・データ構造そのもの ・ロジックを持たせない ・import UIKitだめ、絶対 UIを気にしない //対応がわかりやすいように置き換え typealias GitHubSearchEntityResponse = GitHubResponse typealias GitHubSearchEntity = GitHubSearch typealias GitHubSearchntityError = GitHubError import Foundation struct GitHubResponse: Codable { let items: [GitHubSearch]? } struct GitHubSearch: Codable { let id: Int let name: String private let fullName: String var urlString: String { "https://github.com/\(fullName)" } enum CodingKeys: String, CodingKey { case id case name case fullName = "full_name" } } Router ・画面遷移 ・依存性注入(後述) ・VIPERの肝 VIPERでは画面遷移の処理をRouterで行うことにより、Viewの責務を減らせて可読性の向上が望める GitHubSearchRouter import UIKit protocol GitHubSearchWireframe { func showWeb(initParameters: WebUsecaseInitParameters) func showAlert(error: Error) } final class GitHubSearchRouter { private unowned let viewController: UIViewController private init(viewController: UIViewController) { self.viewController = viewController } // Routerが画面遷移を担当しているので、ここに書く static func assembleModules() -> UIViewController { let view = UIStoryboard.gitHubSearch.instantiateInitialViewController() as! GitHubSearchViewController let interactor = GitHubSearchInteractor() let router = GitHubSearchRouter(viewController: view) // presenterが中継役なので、全てと繋げる let presenter = GitHubSearchPresenter(view: view, interactor: interactor, router: router) // viewからpresenterに通知する必要があるため繋ぐ // viewとpresenterは互いが互いを知っている view.inject(presenter: presenter) return view } } // MARK: - GitHubSearchWireframe extension GitHubSearchRouter: GitHubSearchWireframe { func showWeb(initParameters: WebUsecaseInitParameters) { let next = WebRouter.assembleModules(initParameters: initParameters) viewController.show(next: next) } func showAlert(error: Error) { print(error.localizedDescription) } } おわりに その他の処理はGitHubをご覧ください。 次回は依存性注入とクリーンアーキテクチャのDomain層とData層についてまとめたいと思います!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Firebase初心者] Cloud Firestoreは公式動画が最強なのでまとめてみた [データ構造編]

はじめに Google Cloud Firestoreについて調べていたところ公式Youtubeの説明が一番詳しかったので翻訳しつつまとめてみました。 今回はデータベースの構造についてのみの記事です。 執筆:2021年5月2日 基本的なデータ構造 まずはCloud Firestoreの基本的なデータ構造について説明します。 元の動画はこちら Firestoreではまずコレクションがあり、その中にドキュメントを複数持つことができます。 コレクションを複数作ることもでき、ドキュメント内にコレクションを作ることもできます。これをサブコレクションと言います。(画像のReviewsがサブコレクション) サブコレクション内のドキュメントのサブコレクションを作成することもでき、データは最大100レベルまでネストできます。 ドキュメント内のデータ構造は以下の通りで、各項目はフィールドと呼ばれます。 json型のようなmapとarray(配列)も保持できます。 公式ドキュメント https://firebase.google.com/docs/firestore/manage-data/structure-data Cloud Firestoreの6つのルール Firestoreには大きく分けて6つのルールがあり、それを理解した上でデータ構造を決める必要があります。 変更された点もありますが、動画に沿って変更前と変更後の説明をしています。 元の動画はこちら↓ ① ドキュメントの制限 1つのドキュメントに格納できるデータは1MBまで 300ページの本の文章を格納した場合770KB 写真などの格納は難しい フィールドは20,000まで 全てのフィールドにインデックスが発行される仕組みになっているためmapもフィールドにカウントされるので注意 // map: フィールド数4 address: { city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 } // フィールド数3 city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 同じドキュメントへの書き込みは1秒間に1回まで 複数人が同時に1つのドキュメントへ書き込みをしようとすると失敗する 異なるドキュメントへ同時に書き込むのはOK ② ドキュメントは部分的に取得することができない 例えば以下のようなコレクションで本のタイトルだけ取得したい時、同じドキュメント内に本文(contents)が入っているので同時にそれも取得してしまいます。 アプリのパフォーマンス低下 メモリ消費がすごいことになる 電池消費もすごいことになる といったことになってしまい、ユーザーがアンインストールしてしまう原因になるので注意が必要です。 セキュリティルールも同様にドキュメントを部分的に保護することができません。 同じドキュメント内のフィールドで別々のルールを設けたい場合別ドキュメントとして保存する必要があります。 ③ クエリは深い階層のデータまで取得することができない あるドキュメントを取得した時、そのドキュメントのサブコレクションを取得することはありません。 これは余計な読み込みをしない点でメモリ消費の節約にもなる一方、 そのサブコレクションを読み込みたい場合更にクエリを発行しなければいけないためコードが煩雑になる危険を孕んでいます。 ②の例では本のcontentsはチャプターごとのサブコレクションを作って読み込むのが良さそうです。 ここで登場人物もサブコレクション化している場合を考えてみます。 本の詳細を表示するページで登場人物を表示しないのであれば1回の読み込みで足りますが、 登場人物を一緒に表示させたいと思った場合、登場人物のドキュメントの数の回数分読み込まなくてはいけなくなります。 ④ 使用料金は読み込みと書き込みの回数で決まる ③のルールと関連してきますが、サブコレクションが何重にもなっているとその分読み込み回数が増え、料金が増えてしまいます。 なので、一緒に表示させたいデータは基本的に同じドキュメントに入れるのが良いとされています。 しかし、サブコレクションにしたほうが良い場合もあるため費用と効果のバランスを検討しながらデータ構造を決定する必要があります。 ⑤ クエリは1つのコレクション内のドキュメントしか探すことができない → クエリは複数のコレクションにまたがってドキュメントを探すことができる 例えば上記の例で「"GreatExpectations"のPから始まるキャラクターを探す」というのは以下のようにできます。 //"GreatExpectations"のPから始まるキャラクターを探す collection("dickens_books/great_exp/characters") .where(name >= "P") .where(name < "Q") 当初クエリは1つのコレクション内でしかドキュメントを探すことができず、「全ての本の中からキャラクターの名前がOliverの本を探す」というのは複数のコレクションにまたがって探す必要があるためできませんでした。 が、2019年に改善され、今は以下のコードでできるようになっています。 //全ての本の中からキャラクターの名前がOliverの本を探す collectionGroup("characters") .where("name", "==", "Oliver") 注意点 CollectionGroupクエリの実行にはインデックスの設定が必要 一度クエリを実行するとコンソールにURLが表示されるため、そこから簡単に登録できる Firebase Consoleから自分で設定することも可能 インデックスは最大200まで登録可能 同じ名前のコレクション全てでクエリを実行してしまうので命名には気をつけること 上記の例だと全ての "characters" コレクションでクエリを実行している 公式ドキュメント https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query インデックスについてはこちら https://firebase.google.com/docs/firestore/query-data/indexing ではCollectionGroupクエリが使える以前はどうやって「全ての本の中からキャラクターの名前がOliverの本を探す」ことをしていたのかというと2通りのやり方があります。 1. Collection内にmapを保持する方法 //全ての本の中からキャラクターの名前がOliverの本を探す collection("dickens_Books") .where("characters.Oliver", ">=", "") 「occupationがsocialiteのキャラクターを探す」「名前順に並び替える」というようなクエリは実行不可能であり、検索オプションが限られている Map内fieldの値をキーとするのは間違えやすくエラーが起きやすい ドキュメント内の不必要な情報も一緒に取ってきてしまう という理由で推奨されていません。 2. charactersコレクションをtop-levelのコレクションにする //全ての本の中からキャラクターの名前がOliverの本を探す collection("characters") .where("name", "==", "Oliver") //occupationがsocialiteのキャラクターを探す collection("characters") .where("occupation", "==", "Socialite") //"GreatExpectations"の登場人物を探して名前順に並び替える //2つ以上のクエリを実行するにはインデックスの設定が必要 collection("characters") .where("book", "==", "great_exp").orderBy("name") 幅広い検索が容易にできるためこちらの方法が推奨されていました。 top levelのコレクションから検索する場合検索量が増えてパフォーマンスが悪くなるのではないかと考える人もいると思いますが、サブコレクションを検索する場合と速度にほとんど差はないそうです。 結局どれを使えば良いのか 現時点(2021年5月2日時点)では、以下がどの方法を使うかを決めるガイドラインだと言っています。 ドキュメントの中のあるフィールドについて検索したい場合コレクションに追加する サブコレクションでもtop-levelのコレクションでも可能 サブコレクションごとに検索することが多く、たまにcollectionGroupクエリを使いたい時はサブコレクション 全てのドキュメントにまたがって検索することが多く、サブコレクションごとに検索する場合が少ない時はtop-levelのコレクション 親ドキュメントを読み込んだ際に必ずそのデータが必要になる時はmapを使う 簡単なフローチャートを作成してみました。 ⑥ Arrayの挙動が異質 Firestoreでは同じデータに複数の端末からアクセスすることが可能であり、 インデックス操作でデータを書き換えることが容易なArrayは現在どのフィールドで何の書き込みがされているのか把握することが難しく、思った通りの結果が得られない可能性があります。 例えばAさんが配列の2番目に書き込みをしようとしている時に、Bさんは配列の2番目の要素を削除しようとしており、更にCさんは配列の一番最初にデータを挿入しようとしている場合、それぞれで違う結果が得られてしまいクラッシュしてしまいます。 そのため以下のような配列操作や検索方法は使えません。 my_array[2] = "tackle" my_array.deleteAt(2) my_array.insertAt(0, "hello") collection.where(my_array[2] == "larry") じゃあArrayはどのように使えばいいのかというと、flagの管理用として使用します。 以下は"drama"というキーワードを持つ本を検索しています。 カテゴリ分けの際に使えそうです。 collection("dickens_books").where("keywords", "array-contains", "drama") とはいえ、現在はインデックスに関係なく配列にデータを追加する、削除するarrayUnionとarrayRemoveが追加されたので異なる結果になりクラッシュするということは無くなったと言っています。 詳細はこちら https://firebase.google.com/docs/firestore/manage-data/add-data まとめ Firestoreデータ構造の決め方 動画の最後でデータ構造を決めるガイドラインについて触れていました。 同時に使用するデータは全て同じドキュメント内に保存する 丁度いいサイズで格納する 例として1つの画面表示のために2つの異なるデータベースから30ドキュメントを取得する必要がある設計は細かすぎると言っていました 一部を検索する必要があるもの、今後データが増えていく可能性があるものはコレクションにする 逆にそのデータから親ドキュメントの中身を検索したい場合はmapを親ドキュメントに格納する 住所や位置情報など関連が深いデータのまとまりもmapを使うと良い フィールドで保存する場合よりもキーワードのコンフリクトを避けることができる flagを管理したいときはArrayを使う また動画の続きを引き続き記事にしていきたいと思います。 次はレビュー機能、お気に入り機能付きのレビューアプリのデータ構造を考えていくという実践的な内容です。 間違っている箇所などありましたらコメントでお知らせください。 参考になった記事 【Firebase】Cloud Firestoreのデータ構造の決め方をFirebaseの動画から学ぶ Cloud Firestore公式Youtube
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Firebase初心者] Cloud Firestore公式動画が最強なのでまとめてみた [データ構造編]

はじめに Google Cloud Firestoreについて調べていたところ公式Youtubeの説明が一番詳しかったので翻訳しつつまとめてみました。 今回はデータベースの構造についてのみの記事です。 執筆:2021年5月2日 基本的なデータ構造 まずはCloud Firestoreの基本的なデータ構造について説明します。 元の動画はこちら Firestoreではまずコレクションがあり、その中にドキュメントを複数持つことができます。 コレクションを複数作ることもでき、ドキュメント内にコレクションを作ることもできます。これをサブコレクションと言います。(画像のReviewsがサブコレクション) サブコレクション内のドキュメントのサブコレクションを作成することもでき、データは最大100レベルまでネストできます。 ドキュメント内のデータ構造は以下の通りで、各項目はフィールドと呼ばれます。 json型のようなmapとarray(配列)も保持できます。 公式ドキュメント https://firebase.google.com/docs/firestore/manage-data/structure-data Cloud Firestoreの6つのルール Firestoreには大きく分けて6つのルールがあり、それを理解した上でデータ構造を決める必要があります。 変更された点もありますが、動画に沿って変更前と変更後の説明をしています。 元の動画はこちら↓ ① ドキュメントの制限 1つのドキュメントに格納できるデータは1MBまで 300ページの本の文章を格納した場合770KB 写真などの格納は難しい フィールドは20,000まで 全てのフィールドにインデックスが発行される仕組みになっているためmapもフィールドにカウントされるので注意 // map: フィールド数4 address: { city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 } // フィールド数3 city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 同じドキュメントへの書き込みは1秒間に1回まで 複数人が同時に1つのドキュメントへ書き込みをしようとすると失敗する 異なるドキュメントへ同時に書き込むのはOK ② ドキュメントは部分的に取得することができない 例えば以下のようなコレクションで本のタイトルだけ取得したい時、同じドキュメント内に本文(contents)が入っているので同時にそれも取得してしまいます。 アプリのパフォーマンス低下 メモリ消費がすごいことになる 電池消費もすごいことになる といったことになってしまい、ユーザーがアンインストールしてしまう原因になるので注意が必要です。 セキュリティルールも同様にドキュメントを部分的に保護することができません。 同じドキュメント内のフィールドで別々のルールを設けたい場合別ドキュメントとして保存する必要があります。 ③ クエリは深い階層のデータまで取得することができない あるドキュメントを取得した時、そのドキュメントのサブコレクションを取得することはありません。 これは余計な読み込みをしない点でメモリ消費の節約にもなる一方、 そのサブコレクションを読み込みたい場合更にクエリを発行しなければいけないためコードが煩雑になる危険を孕んでいます。 ②の例では本のcontentsはチャプターごとのサブコレクションを作って読み込むのが良さそうです。 ここで登場人物もサブコレクション化している場合を考えてみます。 本の詳細を表示するページで登場人物を表示しないのであれば1回の読み込みで足りますが、 登場人物を一緒に表示させたいと思った場合、登場人物のドキュメントの数の回数分読み込まなくてはいけなくなります。 ④ 使用料金は読み込みと書き込みの回数で決まる ③のルールと関連してきますが、サブコレクションが何重にもなっているとその分読み込み回数が増え、料金が増えてしまいます。 なので、一緒に表示させたいデータは基本的に同じドキュメントに入れるのが良いとされています。 しかし、サブコレクションにしたほうが良い場合もあるため費用と効果のバランスを検討しながらデータ構造を決定する必要があります。 ⑤ クエリは1つのコレクション内のドキュメントしか探すことができない → クエリは複数のコレクションにまたがってドキュメントを探すことができる 例えば上記の例で「"GreatExpectations"のPから始まるキャラクターを探す」というのは以下のようにできます。 //"GreatExpectations"のPから始まるキャラクターを探す collection("dickens_books/great_exp/characters") .where(name >= "P") .where(name < "Q") 当初クエリは1つのコレクション内でしかドキュメントを探すことができず、「全ての本の中からキャラクターの名前がOliverの本を探す」というのは複数のコレクションにまたがって探す必要があるためできませんでした。 が、2019年に改善され、今は以下のコードでできるようになっています。 //全ての本の中からキャラクターの名前がOliverの本を探す collectionGroup("characters") .where("name", "==", "Oliver") 注意点 CollectionGroupクエリの実行にはインデックスの設定が必要 一度クエリを実行するとコンソールにURLが表示されるため、そこから簡単に登録できる Firebase Consoleから自分で設定することも可能 インデックスは最大200まで登録可能 同じ名前のコレクション全てでクエリを実行してしまうので命名には気をつけること 上記の例だと全ての "characters" コレクションでクエリを実行している 公式ドキュメント https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query インデックスについてはこちら https://firebase.google.com/docs/firestore/query-data/indexing ではCollectionGroupクエリが使える以前はどうやって「全ての本の中からキャラクターの名前がOliverの本を探す」ことをしていたのかというと2通りのやり方があります。 1. Collection内にmapを保持する方法 //全ての本の中からキャラクターの名前がOliverの本を探す collection("dickens_Books") .where("characters.Oliver", ">=", "") 「occupationがsocialiteのキャラクターを探す」「名前順に並び替える」というようなクエリは実行不可能であり、検索オプションが限られている Map内fieldの値をキーとするのは間違えやすくエラーが起きやすい ドキュメント内の不必要な情報も一緒に取ってきてしまう という理由で推奨されていません。 2. charactersコレクションをtop-levelのコレクションにする //全ての本の中からキャラクターの名前がOliverの本を探す collection("characters") .where("name", "==", "Oliver") //occupationがsocialiteのキャラクターを探す collection("characters") .where("occupation", "==", "Socialite") //"GreatExpectations"の登場人物を探して名前順に並び替える //2つ以上のクエリを実行するにはインデックスの設定が必要 collection("characters") .where("book", "==", "great_exp").orderBy("name") 幅広い検索が容易にできるためこちらの方法が推奨されていました。 top levelのコレクションから検索する場合検索量が増えてパフォーマンスが悪くなるのではないかと考える人もいると思いますが、サブコレクションを検索する場合と速度にほとんど差はないそうです。 結局どれを使えば良いのか 現時点(2021年5月2日時点)では、以下がどの方法を使うかを決めるガイドラインだと言っています。 ドキュメントの中のあるフィールドについて検索したい場合コレクションに追加する サブコレクションでもtop-levelのコレクションでも可能 サブコレクションごとに検索することが多く、たまにcollectionGroupクエリを使いたい時はサブコレクション 全てのドキュメントにまたがって検索することが多く、サブコレクションごとに検索する場合が少ない時はtop-levelのコレクション 親ドキュメントを読み込んだ際に必ずそのデータが必要になる時はmapを使う 簡単なフローチャートを作成してみました。 ⑥ Arrayの挙動が異質 Firestoreでは同じデータに複数の端末からアクセスすることが可能であり、 インデックス操作でデータを書き換えることが容易なArrayは現在どのフィールドで何の書き込みがされているのか把握することが難しく、思った通りの結果が得られない可能性があります。 例えばAさんが配列の2番目に書き込みをしようとしている時に、Bさんは配列の2番目の要素を削除しようとしており、更にCさんは配列の一番最初にデータを挿入しようとしている場合、それぞれで違う結果が得られてしまいクラッシュしてしまいます。 そのため以下のような配列操作や検索方法は使えません。 my_array[2] = "tackle" my_array.deleteAt(2) my_array.insertAt(0, "hello") collection.where(my_array[2] == "larry") じゃあArrayはどのように使えばいいのかというと、flagの管理用として使用します。 以下は"drama"というキーワードを持つ本を検索しています。 カテゴリ分けの際に使えそうです。 collection("dickens_books").where("keywords", "array-contains", "drama") とはいえ、現在はインデックスに関係なく配列にデータを追加する、削除するarrayUnionとarrayRemoveが追加されたので異なる結果になりクラッシュするということは無くなったと言っています。 詳細はこちら https://firebase.google.com/docs/firestore/manage-data/add-data まとめ Firestoreデータ構造の決め方 動画の最後でデータ構造を決めるガイドラインについて触れていました。 同時に使用するデータは全て同じドキュメント内に保存する 丁度いいサイズで格納する 例として1つの画面表示のために2つの異なるデータベースから30ドキュメントを取得する必要がある設計は細かすぎると言っていました 一部を検索する必要があるもの、今後データが増えていく可能性があるものはコレクションにする 逆にそのデータから親ドキュメントの中身を検索したい場合はmapを親ドキュメントに格納する 住所や位置情報など関連が深いデータのまとまりもmapを使うと良い フィールドで保存する場合よりもキーワードのコンフリクトを避けることができる flagを管理したいときはArrayを使う また動画の続きを引き続き記事にしていきたいと思います。 次はレビュー機能、お気に入り機能付きのレビューアプリのデータ構造を考えていくという実践的な内容です。 間違っている箇所などありましたらコメントでお知らせください。 参考になった記事 【Firebase】Cloud Firestoreのデータ構造の決め方をFirebaseの動画から学ぶ Cloud Firestore公式Youtube
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Firebase初心者] Cloud Firestore公式動画をまとめてみた [データ構造編]

はじめに Google Cloud Firestoreについて調べていたところ公式Youtubeの説明が一番詳しかったので翻訳しつつまとめてみました。 今回はデータベースの構造についてのみの記事です。 執筆:2021年5月2日 基本的なデータ構造 まずはCloud Firestoreの基本的なデータ構造について説明します。 元の動画はこちら Firestoreではまずコレクションがあり、その中にドキュメントを複数持つことができます。 コレクションを複数作ることもでき、ドキュメント内にコレクションを作ることもできます。これをサブコレクションと言います。(画像のReviewsがサブコレクション) サブコレクション内のドキュメントのサブコレクションを作成することもでき、データは最大100レベルまでネストできます。 ドキュメント内のデータ構造は以下の通りで、各項目はフィールドと呼ばれます。 json型のようなmapとarray(配列)も保持できます。 公式ドキュメント https://firebase.google.com/docs/firestore/manage-data/structure-data Cloud Firestoreの6つのルール Firestoreには大きく分けて6つのルールがあり、それを理解した上でデータ構造を決める必要があります。 変更された点もありますが、動画に沿って変更前と変更後の説明をしています。 元の動画はこちら↓ ① ドキュメントの制限 1つのドキュメントに格納できるデータは1MBまで 300ページの本の文章を格納した場合770KB 写真などの格納は難しい フィールドは20,000まで 全てのフィールドにインデックスが発行される仕組みになっているためmapもフィールドにカウントされるので注意 // map: フィールド数4 address: { city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 } // フィールド数3 city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 同じドキュメントへの書き込みは1秒間に1回まで 複数人が同時に1つのドキュメントへ書き込みをしようとすると失敗する 異なるドキュメントへ同時に書き込むのはOK ② ドキュメントは部分的に取得することができない 例えば以下のようなコレクションで本のタイトルだけ取得したい時、同じドキュメント内に本文(contents)が入っているので同時にそれも取得してしまいます。 アプリのパフォーマンス低下 メモリ消費がすごいことになる 電池消費もすごいことになる といったことになってしまい、ユーザーがアンインストールしてしまう原因になるので注意が必要です。 セキュリティルールも同様にドキュメントを部分的に保護することができません。 同じドキュメント内のフィールドで別々のルールを設けたい場合別ドキュメントとして保存する必要があります。 ③ クエリは深い階層のデータまで取得することができない あるドキュメントを取得した時、そのドキュメントのサブコレクションを取得することはありません。 これは余計な読み込みをしない点でメモリ消費の節約にもなる一方、 そのサブコレクションを読み込みたい場合更にクエリを発行しなければいけないためコードが煩雑になる危険を孕んでいます。 ②の例では本のcontentsはチャプターごとのサブコレクションを作って読み込むのが良さそうです。 ここで登場人物もサブコレクション化している場合を考えてみます。 本の詳細を表示するページで登場人物を表示しないのであれば1回の読み込みで足りますが、 登場人物を一緒に表示させたいと思った場合、登場人物のドキュメントの数の回数分読み込まなくてはいけなくなります。 ④ 使用料金は読み込みと書き込みの回数で決まる ③のルールと関連してきますが、サブコレクションが何重にもなっているとその分読み込み回数が増え、料金が増えてしまいます。 なので、一緒に表示させたいデータは基本的に同じドキュメントに入れるのが良いとされています。 しかし、サブコレクションにしたほうが良い場合もあるため費用と効果のバランスを検討しながらデータ構造を決定する必要があります。 ⑤ クエリは1つのコレクション内のドキュメントしか探すことができない → クエリは複数のコレクションにまたがってドキュメントを探すことができる 例えば上記の例で「"GreatExpectations"のPから始まるキャラクターを探す」というのは以下のようにできます。 //"GreatExpectations"のPから始まるキャラクターを探す collection("dickens_books/great_exp/characters") .where(name >= "P") .where(name < "Q") 当初クエリは1つのコレクション内でしかドキュメントを探すことができず、「全ての本の中からキャラクターの名前がOliverの本を探す」というのは複数のコレクションにまたがって探す必要があるためできませんでした。 が、2019年に改善され、今は以下のコードでできるようになっています。 //全ての本の中からキャラクターの名前がOliverの本を探す collectionGroup("characters") .where("name", "==", "Oliver") 注意点 CollectionGroupクエリの実行にはインデックスの設定が必要 一度クエリを実行するとコンソールにURLが表示されるため、そこから簡単に登録できる Firebase Consoleから自分で設定することも可能 インデックスは最大200まで登録可能 同じ名前のコレクション全てでクエリを実行してしまうので命名には気をつけること 上記の例だと全ての "characters" コレクションでクエリを実行している 公式ドキュメント https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query インデックスについてはこちら https://firebase.google.com/docs/firestore/query-data/indexing ではCollectionGroupクエリが使える以前はどうやって「全ての本の中からキャラクターの名前がOliverの本を探す」ことをしていたのかというと2通りのやり方があります。 1. Collection内にmapを保持する方法 //全ての本の中からキャラクターの名前がOliverの本を探す collection("dickens_Books") .where("characters.Oliver", ">=", "") 「occupationがsocialiteのキャラクターを探す」「名前順に並び替える」というようなクエリは実行不可能であり、検索オプションが限られている Map内fieldの値をキーとするのは間違えやすくエラーが起きやすい ドキュメント内の不必要な情報も一緒に取ってきてしまう という理由で推奨されていません。 2. charactersコレクションをtop-levelのコレクションにする //全ての本の中からキャラクターの名前がOliverの本を探す collection("characters") .where("name", "==", "Oliver") //occupationがsocialiteのキャラクターを探す collection("characters") .where("occupation", "==", "Socialite") //"GreatExpectations"の登場人物を探して名前順に並び替える //2つ以上のクエリを実行するにはインデックスの設定が必要 collection("characters") .where("book", "==", "great_exp").orderBy("name") 幅広い検索が容易にできるためこちらの方法が推奨されていました。 top levelのコレクションから検索する場合検索量が増えてパフォーマンスが悪くなるのではないかと考える人もいると思いますが、サブコレクションを検索する場合と速度にほとんど差はないそうです。 結局どれを使えば良いのか 現時点(2021年5月2日時点)では、以下がどの方法を使うかを決めるガイドラインだと言っています。 ドキュメントの中のあるフィールドについて検索したい場合コレクションに追加する サブコレクションでもtop-levelのコレクションでも可能 サブコレクションごとに検索することが多く、たまにcollectionGroupクエリを使いたい時はサブコレクション 全てのドキュメントにまたがって検索することが多く、サブコレクションごとに検索する場合が少ない時はtop-levelのコレクション 親ドキュメントを読み込んだ際に必ずそのデータが必要になる時はmapを使う 簡単なフローチャートを作成してみました。 ⑥ Arrayの挙動が異質 Firestoreでは同じデータに複数の端末からアクセスすることが可能であり、 インデックス操作でデータを書き換えることが容易なArrayは現在どのフィールドで何の書き込みがされているのか把握することが難しく、思った通りの結果が得られない可能性があります。 例えばAさんが配列の2番目に書き込みをしようとしている時に、Bさんは配列の2番目の要素を削除しようとしており、更にCさんは配列の一番最初にデータを挿入しようとしている場合、それぞれで違う結果が得られてしまいクラッシュしてしまいます。 そのため以下のような配列操作や検索方法は使えません。 my_array[2] = "tackle" my_array.deleteAt(2) my_array.insertAt(0, "hello") collection.where(my_array[2] == "larry") じゃあArrayはどのように使えばいいのかというと、flagの管理用として使用します。 以下は"drama"というキーワードを持つ本を検索しています。 カテゴリ分けの際に使えそうです。 collection("dickens_books").where("keywords", "array-contains", "drama") とはいえ、現在はインデックスに関係なく配列にデータを追加する、削除するarrayUnionとarrayRemoveが追加されたので異なる結果になりクラッシュするということは無くなったと言っています。 詳細はこちら https://firebase.google.com/docs/firestore/manage-data/add-data まとめ Firestoreデータ構造の決め方 動画の最後でデータ構造を決めるガイドラインについて触れていました。 同時に使用するデータは全て同じドキュメント内に保存する 丁度いいサイズで格納する 例として1つの画面表示のために2つの異なるデータベースから30ドキュメントを取得する必要がある設計は細かすぎると言っていました 一部を検索する必要があるもの、今後データが増えていく可能性があるものはコレクションにする 逆にそのデータから親ドキュメントの中身を検索したい場合はmapを親ドキュメントに格納する 住所や位置情報など関連が深いデータのまとまりもmapを使うと良い フィールドで保存する場合よりもキーワードのコンフリクトを避けることができる flagを管理したいときはArrayを使う また動画の続きを引き続き記事にしていきたいと思います。 次はレビュー機能、お気に入り機能付きのレビューアプリのデータ構造を考えていくという実践的な内容です。 間違っている箇所などありましたらコメントでお知らせください。 参考になった記事 【Firebase】Cloud Firestoreのデータ構造の決め方をFirebaseの動画から学ぶ Cloud Firestore公式Youtube
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Firebase] Cloud Firestoreのデータ構造について動画の内容をまとめてみた

はじめに Google Cloud Firestoreについて調べていたところ公式Youtubeの説明が一番詳しかったので翻訳しつつまとめてみました。 今回はデータベースの構造についてのみの記事です。 執筆:2021年5月2日 基本的なデータ構造 まずはCloud Firestoreの基本的なデータ構造について説明します。 元の動画はこちら Firestoreではまずコレクションがあり、その中にドキュメントを複数持つことができます。 コレクションを複数作ることもでき、ドキュメント内にコレクションを作ることもできます。これをサブコレクションと言います。(画像のReviewsがサブコレクション) サブコレクション内のドキュメントのサブコレクションを作成することもでき、データは最大100レベルまでネストできます。 ドキュメント内のデータ構造は以下の通りで、各項目はフィールドと呼ばれます。 json型のようなmapとarray(配列)も保持できます。 公式ドキュメント https://firebase.google.com/docs/firestore/manage-data/structure-data Cloud Firestoreの6つのルール Firestoreには大きく分けて6つのルールがあり、それを理解した上でデータ構造を決める必要があります。 変更された点もありますが、動画に沿って変更前と変更後の説明をしています。 元の動画はこちら↓ ① ドキュメントの制限 1つのドキュメントに格納できるデータは1MBまで 300ページの本の文章を格納した場合770KB 写真などの格納は難しい フィールドは20,000まで 全てのフィールドにインデックスが発行される仕組みになっているためmapもフィールドにカウントされるので注意 // map: フィールド数4 address: { city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 } // フィールド数3 city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 同じドキュメントへの書き込みは1秒間に1回まで 複数人が同時に1つのドキュメントへ書き込みをしようとすると失敗する 異なるドキュメントへ同時に書き込むのはOK ② ドキュメントは部分的に取得することができない 例えば以下のようなコレクションで本のタイトルだけ取得したい時、同じドキュメント内に本文(contents)が入っているので同時にそれも取得してしまいます。 アプリのパフォーマンス低下 メモリ消費がすごいことになる 電池消費もすごいことになる といったことになってしまい、ユーザーがアンインストールしてしまう原因になるので注意が必要です。 セキュリティルールも同様にドキュメントを部分的に保護することができません。 同じドキュメント内のフィールドで別々のルールを設けたい場合別ドキュメントとして保存する必要があります。 ③ クエリは深い階層のデータまで取得することができない あるドキュメントを取得した時、そのドキュメントのサブコレクションを取得することはありません。 これは余計な読み込みをしない点でメモリ消費の節約にもなる一方、 そのサブコレクションを読み込みたい場合更にクエリを発行しなければいけないためコードが煩雑になる危険を孕んでいます。 ②の例では本のcontentsはチャプターごとのサブコレクションを作って読み込むのが良さそうです。 ここで登場人物もサブコレクション化している場合を考えてみます。 本の詳細を表示するページで登場人物を表示しないのであれば1回の読み込みで足りますが、 登場人物を一緒に表示させたいと思った場合、登場人物のドキュメントの数の回数分読み込まなくてはいけなくなります。 ④ 使用料金は読み込みと書き込みの回数で決まる ③のルールと関連してきますが、サブコレクションが何重にもなっているとその分読み込み回数が増え、料金が増えてしまいます。 なので、一緒に表示させたいデータは基本的に同じドキュメントに入れるのが良いとされています。 しかし、サブコレクションにしたほうが良い場合もあるため費用と効果のバランスを検討しながらデータ構造を決定する必要があります。 ⑤ クエリは1つのコレクション内のドキュメントしか探すことができない → クエリは複数のコレクションにまたがってドキュメントを探すことができる 例えば上記の例で「"GreatExpectations"のPから始まるキャラクターを探す」というのは以下のようにできます。 //"GreatExpectations"のPから始まるキャラクターを探す collection("dickens_books/great_exp/characters") .where(name >= "P") .where(name < "Q") 当初クエリは1つのコレクション内でしかドキュメントを探すことができず、「全ての本の中からキャラクターの名前がOliverの本を探す」というのは複数のコレクションにまたがって探す必要があるためできませんでした。 が、2019年に改善され、今は以下のコードでできるようになっています。 //全ての本の中からキャラクターの名前がOliverの本を探す collectionGroup("characters") .where("name", "==", "Oliver") 注意点 CollectionGroupクエリの実行にはインデックスの設定が必要 一度クエリを実行するとコンソールにURLが表示されるため、そこから簡単に登録できる Firebase Consoleから自分で設定することも可能 インデックスは最大200まで登録可能 同じ名前のコレクション全てでクエリを実行してしまうので命名には気をつけること 上記の例だと全ての "characters" コレクションでクエリを実行している 公式ドキュメント https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query インデックスについてはこちら https://firebase.google.com/docs/firestore/query-data/indexing ではCollectionGroupクエリが使える以前はどうやって「全ての本の中からキャラクターの名前がOliverの本を探す」ことをしていたのかというと2通りのやり方があります。 1. Collection内にmapを保持する方法 //全ての本の中からキャラクターの名前がOliverの本を探す collection("dickens_Books") .where("characters.Oliver", ">=", "") 「occupationがsocialiteのキャラクターを探す」「名前順に並び替える」というようなクエリは実行不可能であり、検索オプションが限られている Map内fieldの値をキーとするのは間違えやすくエラーが起きやすい ドキュメント内の不必要な情報も一緒に取ってきてしまう という理由で推奨されていません。 2. charactersコレクションをtop-levelのコレクションにする //全ての本の中からキャラクターの名前がOliverの本を探す collection("characters") .where("name", "==", "Oliver") //occupationがsocialiteのキャラクターを探す collection("characters") .where("occupation", "==", "Socialite") //"GreatExpectations"の登場人物を探して名前順に並び替える //2つ以上のクエリを実行するにはインデックスの設定が必要 collection("characters") .where("book", "==", "great_exp").orderBy("name") 幅広い検索が容易にできるためこちらの方法が推奨されていました。 top levelのコレクションから検索する場合検索量が増えてパフォーマンスが悪くなるのではないかと考える人もいると思いますが、サブコレクションを検索する場合と速度にほとんど差はないそうです。 結局どれを使えば良いのか 現時点(2021年5月2日時点)では、以下がどの方法を使うかを決めるガイドラインだと言っています。 ドキュメントの中のあるフィールドについて検索したい場合コレクションに追加する サブコレクションでもtop-levelのコレクションでも可能 サブコレクションごとに検索することが多く、たまにcollectionGroupクエリを使いたい時はサブコレクション 全てのドキュメントにまたがって検索することが多く、サブコレクションごとに検索する場合が少ない時はtop-levelのコレクション 親ドキュメントを読み込んだ際に必ずそのデータが必要になる時はmapを使う 簡単なフローチャートを作成してみました。 ⑥ Arrayの挙動が異質 Firestoreでは同じデータに複数の端末からアクセスすることが可能であり、 インデックス操作でデータを書き換えることが容易なArrayは現在どのフィールドで何の書き込みがされているのか把握することが難しく、思った通りの結果が得られない可能性があります。 例えばAさんが配列の2番目に書き込みをしようとしている時に、Bさんは配列の2番目の要素を削除しようとしており、更にCさんは配列の一番最初にデータを挿入しようとしている場合、それぞれで違う結果が得られてしまいクラッシュしてしまいます。 そのため以下のような配列操作や検索方法は使えません。 my_array[2] = "tackle" my_array.deleteAt(2) my_array.insertAt(0, "hello") collection.where(my_array[2] == "larry") じゃあArrayはどのように使えばいいのかというと、flagの管理用として使用します。 以下は"drama"というキーワードを持つ本を検索しています。 カテゴリ分けの際に使えそうです。 collection("dickens_books").where("keywords", "array-contains", "drama") とはいえ、現在はインデックスに関係なく配列にデータを追加する、削除するarrayUnionとarrayRemoveが追加されたので異なる結果になりクラッシュするということは無くなったと言っています。 詳細はこちら https://firebase.google.com/docs/firestore/manage-data/add-data まとめ Firestoreデータ構造の決め方 動画の最後でデータ構造を決めるガイドラインについて触れていました。 同時に使用するデータは全て同じドキュメント内に保存する 丁度いいサイズで格納する 例として1つの画面表示のために2つの異なるデータベースから30ドキュメントを取得する必要がある設計は細かすぎると言っていました 一部を検索する必要があるもの、今後データが増えていく可能性があるものはコレクションにする 逆にそのデータから親ドキュメントの中身を検索したい場合はmapを親ドキュメントに格納する 住所や位置情報など関連が深いデータのまとまりもmapを使うと良い フィールドで保存する場合よりもキーワードのコンフリクトを避けることができる flagを管理したいときはArrayを使う また動画の続きを引き続き記事にしていきたいと思います。 次はレビュー機能、お気に入り機能付きのレビューアプリのデータ構造を考えていくという実践的な内容です。 間違っている箇所などありましたらコメントでお知らせください。 参考になった記事 【Firebase】Cloud Firestoreのデータ構造の決め方をFirebaseの動画から学ぶ Cloud Firestore公式Youtube
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む