Firestoreでcountの集計がとれるようになっていたのでiOSで実装してみた

こんにちは。[virapture株式会社](https://virapture.com)でCEOしながら[ラグナロク株式会社](https://ragna-rock.com)でもCKOとして働いている[@mogmet](https://twitter.com/mogmet)です。
![mogmet.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42234/8d49d5b8-5312-bf3c-856f-c3cf02901f69.jpeg)
一昔前の聖地巡礼したときの写真がgoogleにサジェストされました。あの花はとても名作なので是非みてない人は見てください。

本日はmonoさんが衝撃な機能をツイートしていたので実際に試してみた記事になります。（monoさん素敵な情報ツイートありがとうございますー！！）

https://twitter.com/_mono/status/1579729394754813953?s=20&t=mgpuz2DMTgcIFfamOYL5tQ

というわけで、Firebaseのバージョン10.0.0から集計系の機能が入ってきたので早速紹介します。

https://firebase.google.com/support/release-notes/ios

## Firestoreの **count** について

いままでFirestoreでcountを取得するためには全件取得するか、count用のフィールドを作って、ドキュメントが作られるたびにインクリメントするみたいな処理を作る必要がありました。
しかし、今回のこの機能を使えばさらっとcountがとれてしまうという素敵な機能になります！！

公式ドキュメントによるとQueryにcountが増えているようです。
https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/Query#/c:objc(cs)FIRQuery(py)count

## 実装

今回のサンプルは下記リポジトリに置いておきました。

https://github.com/mogmet/FirestoreCount

まず初期化します。

```swift
import SwiftUI
import Firebase
import FirebaseFirestore

@main
struct FirestoreCountApp: App {
    init() {
        FirebaseApp.configure()
        // emulatorを使わない場合は下記settingsをすべてコメントアウトする
        let settings = Firestore.firestore().settings
        settings.host = "localhost:8080"
        settings.isPersistenceEnabled = false
        settings.isSSLEnabled = false
        Firestore.firestore().settings = settings
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

実装はこんな感じ。

```swift
import SwiftUI
import FirebaseFirestore

struct ContentView: View {
    @State var countText: String = "none"
    var body: some View {
        VStack {
            Text(countText)
            Button {
                Task {
                    await loadCount()
                }
            } label: {
                Text("Load count")
            }
        }
        .padding()
    }
    func loadCount() async {
        let aggregateSnapshot = try! await Firestore.firestore().collection("users").count.getAggregation(source: .server)
        countText = aggregateSnapshot.count.stringValue
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

```

ここが肝になります。countがニョキッと生えてます。

```swift
let aggregateSnapshot = try! await Firestore.firestore()
    .collection("users").count.getAggregation(source: .server)
```


firestoreのデータの準備をしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42234/78b07d56-a974-3c8c-a684-eb40f41dfaf3.png)

ボタンを押して挙動を確認してみます。

|before|after|
|-|-|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42234/76979ea7-9651-8c46-8fc6-7c4c20f1430b.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42234/e8bc3550-c173-0082-10d8-611f812356f4.png)|


## リードコストは少なくなるのか？

**多分** 少なくなるというのが結論です。

実際に挙動を試してみたのですが、読み取りが一切カウントされませんでした。（webがまだcountに対応してない？）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42234/002e1e2a-5225-e17c-dc0b-a55953377c08.png)
（ちなみにぴょんとreadが飛び上がってるのはwebコンソールを開いてデータを確認したため上がってるだけです。）

emulatorもgetDocumentsしたときと結果がかわらなくて本当にリードコストが少なくなってるかは謎です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42234/43685c0b-6a07-a6fe-dac6-890a16d0c2f7.png)

一応[ドキュメント](https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/Query#/c:objc(cs)FIRQuery(py)count)では下記の通り説明があります。

> A query that counts the documents in the result set of this query.
> 
> The AggregateQuery query, when executed, counts the documents in the result set of this Query without actually downloading the documents.
> 
> Using the returned query to count the documents is efficient because only the final count, not the documents’ data, is downloaded. The returned query can even count the documents if the result set would be prohibitively large to download entirely (e.g. thousands of documents).

ざっくりいうと実際にダウンロードすることなく結果のカウントだけするから効率的だぜと言ってます。

なのでおそらく1read換算になるのでは・・・・？という判断です。

## リアルタイム取得はできる？

結論でいうと **多分まだできない？** になります。

理由としては[AggregateQuery](https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/AggregateQuery)にListenerを返すメソッドがないからになります。

ただ、AggregateQuery自体には[query](https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/Query)を返すものが生えていてそこをたどればaddSnapshotListenerを呼び出すことはできるのですが、挙動的にいつも通りのコレクションの全リードが発生してそうな雰囲気でした。
(snapshotsのなかにデータが入っていた）

今後拡充される可能性はありますが、もう少し様子見な感じです。

（もし誰かやり方わかったら教えて下さい！）

## まとめ

countがリードコストもかさまずにコールすることができるようになりました！
集計系は今後も増えそうな雰囲気なのでますますfirestoreが便利になっていくので期待です！


最後に、[ワンナイト人狼オンライン](https://online.1nite-jinro.com/link/blog)というゲームを作ってます！よかったら遊んでね！

https://online.1nite-jinro.com/link/blog

他にも[Camecon](https://camecon.me/)や[Offcha](https://offcha.com/)、問い合わせ対応が簡単にできる[CSmart](https://csmart.jp/lp)、フリーランスのコミュニティの[NextFreelance](https://next-freelance.net/)といったサービスも作ってるのでよかったら使ってね！

https://camecon.me/

https://offcha.com/

https://csmart.jp/lp

https://next-freelance.net/

また、チームビルディングや技術顧問、Firebaseの設計やアドバイスといったお話も受け付けてますので御用の方は弊社までお問い合わせください。

https://virapture.com/

ラグナロクでもエンジニアやデザイナーのメンバーを募集しています！！楽しくぶち上げたい人はぜひお話ししましょう！！

https://ragna-rock.com