FireStore の Document Reference Type について

# 最初に
この記事は、 `Swift` で `Firestore` の `Document Reference Type` を、
どのように扱ったかを共有する記事です。

記載時(2017/12/25)のバージョンは下記の通りです。

|対象|バージョン|
|---|---|
|Swift|4.0.3|
|RxSwift|4.0.0|
|Firestore|0.9.3|


# Firestore の Reference Type について

`Firestore`の`Reference Type`は、
`Realtime Database`(以下、`RTDB`) にはない **新しいデータ型** です。

`Reference Type`の役割は、コレクション・ドキュメントのパスを指定することにより、
`Relational Database`の`Foreign key`ほど厳密ではないにしろ、
**整合性を保つためのキー** として役割を担うことができます。

私が`RTDB`ではなく、`Firestore`を使いたいと思う一番の理由は`Reference　Type`があるということです。

# Reference Type を用いたデータの引き方
`Reference Type` には`Collection Reference`と`Document Reference`の二種類あります。
今回は`Document Reference`を用いてどのようにデータを引くのかを、
投稿テーブル (`posts`) と、ユーザーテーブル (`users`) が存在するフィード型アプリを例にご説明いたします。

<img width="516" alt="Screen Shot 2017-12-25 at 14.41.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/30359/a126f5f3-2b86-bfe4-c6ca-3b4f3ec5beca.png">


## データ設計と Entity クラス
### 投稿テーブル (posts) / Firestore
|Field|Data Type|
|---|---|
| title | String |
| userReference | Reference |
### 投稿モデル (Post) / Swift
`userReference`からデータを取得した後にセットする用に、`User`プロパティを用意します。

```swift
struct Post {
	let id: String
	let title: String
	let userReference: DocumentReference
	var user: User?

	init(key: String, dictionary: [String: Any]) throws {
		guard let title = dictionary["title"] as? String,
		let userReference = dictionary["userReference"] as? DocumentReference else { throw FirestoreError.initializeFailed }

		self.id = key
		self.title = title
		self.userReference = userReference
	}
}
```
### ユーザーテーブル (users) / Firestore
|Field|Data Type|
|---|---|
| name | String |
### ユーザーモデル (User) / Swift

```swift
struct User {
	let id: String
	let name: String

	init(key: String, dictionary: [String: Any]) throws {
		guard let name = dictionary["name"] as? String else { throw FirestoreError.initializeFailed }

		self.id = key
		self.name = name
	}
}
```

## アプリからのデータの引き方
`RxSwift` の `Zip` オペレータを利用し、
`Post` に紐づく `User` 情報を結合して取得します。


```swift
fileprivate func observeQuery() {
    Firestore.firestore().collection("posts").addSnapshotListener { [unowned self] (snapshot, error) in
        guard let snapshot = snapshot else { return }
        var posts = snapshot.documents.map {
            try! Post(key: $0.documentID, dictionary: $0.data())
        }

        print(posts) // この時点で user は nil

        let observables: [Observable<User>] = posts.map {
            // userReference を元に user を取得する observe を生成
            self.observeUser(documentRef: $0.userReference)
        }
        Observable.zip(observables).subscribe({
            guard let users = $0.element else { return }
            for user in users {
                for (index, post) in posts.enumerated() where post.userReference.documentID == user.id {
                    posts[index].userEntity = user
                }
            }
            print(posts) // user にデータが入っている
        }).disposed(by: self.disposeBag)
    }
}
```

## 全体のソースコード
`Firestore`用の設定を行った後に以下をコピれば動くと思います。
https://gist.github.com/bpyamasinn/138a6e8264cf181bbf01e2cb162bf73e
全体で 100 行ほどなので、簡単に`Firestore`の仕組みと、`Document Reference`の使い方の参考になると思います。

## 料金に関する注意点
`RTDB`と違い、`Firestore`は **通信量** ではなく、 **通信回数** で料金が発生します。
そのため、`Document Reference`を駆使しすぎるとコストがかかるため、
本当にリレーションが必要な箇所を見極めて設計するのが低コストに抑える必要があると思います。

[Firebase 製品の料金一覧](https://firebase.google.com/pricing/?hl=ja)

例えば`Post`にタグ機能を付与したいと思った場合は、本当にリレーショナルである必要であるのか考慮します。
場合にはよりますが、私の場合`tag`自体、名前の変更は稀なものだと判断し、
`Reference Type`ではなく、`Object Type`で保持する判断をすると思います。

## 最後に
`Firestore` は現時点では **βバージョン** ですが、
`Reference` 型がなくなるということはないと思うので、
`Reference` 型について知っておいて損はないと思います。

## 参考
[サポートされるデータ型  |  Firebase](https://firebase.google.com/docs/firestore/manage-data/data-types?hl=ja)
https://stackoverflow.com/questions/46568850/what-is-firestore-reference-data-type-good-for