【Unity×Cloud Firestore(Firebase)】UnityにおけるFirestoreへの書き込み＆読み込み

## はじめに
Cloud FirestoreはFirebaseの機能の1つです。今までUnityにはSdkが配布されておらず、RealtimeDatabaseの方(こちらにはSdkが配布されていたため)を利用するケースが多かったと思われます。しかし、知らぬ間に__FirestoreのSdkも配布を開始__していました。
とはいえ、だいぶ新しい(2020年に出たっぽい？)こともありUnity(C#)に関しては情報が少ないため、Firebaseなんもわからんな身ながら使い方を備忘録として残しておこうと思います。

ちなみにUnityに限らなければ他の記事でも十分紹介されていると思うので、Firestore自体の構造の説明などは基本省いています(書くのがめんどくさいわけじゃないですよ…？)。
## 情報の書き込み
まずは情報の書き込み方法です。一番簡易的なものは以下の通り。

```.cs
// id未指定(自動で生成されます)
Task writeTask = dataBase.Collection(path).Document().SetAsync(data);

// id指定
Task writeTask = dataBase.Collection(path).Document(id).SetAsync(data);
```

`data`は`Dictionary<string, object>`のもの。ちゃんと終了したのかなどをしっかり確認する場合は上記のようにTaskとして変数に保存して、`task.IsCompleted`が`true`なるまでコルーチンで待機させたり、`～.SetAsync(data).ContinueWith()`(`task.ContinueWith()`でも可)などで対応してあげましょう。

以下は書き込む内容の例。書き込むデータは`Dictionary<string, object>`です。Keyの方はそのままデータベース上のキーになります。大文字、小文字でも異なるので注意しましょう。

```.cs
Dictionary<string, object> data = new Dictionary<string, object>()
{
   {"Name", "Hoge"},
   {"Point", 1},
   {"IsCompleted", true},
};
```


## 情報の読み込み
### 全データ取得
以下のコードは`id`と等しい`Id`のドキュメントにおけるPointフィールドの値を取得してログ出力しているものです。

```AllDataSample.cs
        // dataBase = FirebaseFirestore.DefaultInstance;
        public async void ReadPointAsync(string id)
        {
            int point = 0;
            Query query = dataBase.Collection(userDataPath);
            QuerySnapshot querySnapshot = await query.GetSnapshotAsync();

            foreach (DocumentSnapshot document in querySnapshot.Documents)
            {
                if (document.Id.Equals(id))
                {
                    Debug.Log($"Hit user data.(Id:{document.Id})");
                    Dictionary<string, object> docDictionary = document.ToDictionary();
                    if (docDictionary.ContainsKey("point"))
                    {
                        Debug.Log($"Point:{docDictionary["point"]}");
                    }

                    break;
                }
            }
        }
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/be53958e-e038-49a2-2272-698085118a28.png)
上のスクリーンショットのようなドキュメントがあるとしたら、(Idは等しかったとして)`Point:0`というログ出力が得られるはずです。

`dataBase`は`FirebaseFirestore.DefaultInstance`です。
`userDataPath`はスペルミス防止かつ何度も文字列打つのだるいので、あらかじめ記載コード外の部分で宣言してあるコレクションのパス(今回の例で言うと値は`UserData`)です。なんなら`"point"`とかも全部似たような感じであらかじめ宣言してあるんですが、あんまり事前にメソッド外で宣言した変数ばっかになると備忘録の際はわかりにくいかなと思ったので、今回は直に記述し直しました。

### 結果を返すメソッドにする
先ほどのは`void`なので、取得したデータを返したりはしていませんでした。もちろん、先ほどのメソッド内でデータセットを行うこともできますが、一応他のクラスから使用する場合なども考えて、結果を返すメソッドにしてみます。

```AllDataReturnSample.cs.cs
        public async Task<int> ReadPointAsync(string id)
        {
            int point = 0;
            Query query = dataBase.Collection(userDataPath);
            QuerySnapshot querySnapshot = await query.GetSnapshotAsync();

            foreach (DocumentSnapshot document in querySnapshot.Documents)
            {
                if (document.Id.Equals(id))
                {
                    Debug.Log($"Hit user data.(Id:{document.Id})");
                    Dictionary<string, object> docDictionary = document.ToDictionary();
                    if (docDictionary.ContainsKey("point"))
                    {
                        Debug.Log($"Point:{docDictionary["point"]}");
                        point = (int) Convert.ChangeType(docDictionary["point"], typeof(int));
                    }

                    break;
                }
            }
            return point;
        }
```

まあ、ほとんど変わらないんですけど…、使用例は以下の通り。

```example0.cs
            IEnumerator loadingData()
            {
                Task<List<int>> task = firebaseController.ReadPointAsync();
                while (!task.IsCompleted)
                {
                    yield return null;
                }
                point = task.Result;
            }

            StartCoroutine(loadingData());
```

もしくは、以下のようにContinueWith()を使うこともできます。

```example1.cs
firebaseController.ReadPointAsync().ContinueWith(task => { point = task.Result; });
```

### 得た情報のCast
stringなら、`name = dictionary["path"].ToString();`などで問題ありませんが、そのほかの型の場合はダウンキャストになり色々めんどくさいです。Systemの`Convert.ChangeType()`を用いるのが楽です。

```CastExample.cs
// need "using System;"
Dictionary<string, object> dictionary = (Dictionary<string, object>) Convert.ChangeType(
                            docDictionary["path"],
                            typeof(Dictionary<string, object>));
```

上記はFirestore側でmapとして保存してあるデータを変換する場合。

一応そのほか全種掲載しておきます。

#### フィールドタイプ string
すでに記述済みではありますが、改めて。

```.cs
docDictionary["path"].ToString();
```

#### フィールドタイプ number
`number(数字)`は`int`などに変換できます。`float`とかも同じ感じでいけます。

```.cs
(int) Convert.ChangeType(docDictionary["path"], typeof(int));
```

#### フィールドタイプ boolean
`boolean(ブール値)`は`bool`に変換できます。

```.cs
(bool) Convert.ChangeType(docDictionary["path"], typeof(bool));
```

#### フィールドタイプ map
`map(マップ)`は`Dictionary<string, object>`に変換できます。Valueをいきなり`int`などにしての変換はできないので、まずは`Dictionary<string, object>`に変換してから扱うようにしましょう。

```.cs
(Dictionary<string, object>) Convert.ChangeType(docDictionary["path"], typeof(Dictionary<string, object>));
```

#### フィールドタイプ array
`array(配列)`は配列とは名ばかりに`List<object>`に変換できます。もし、本当に配列として扱いたいのであれば、いったんリストで読み込んだ後に配列に変換してあげる必要があります。

```.cs
(List<object>) Convert.ChangeType(docDictionary["path"], typeof(List<object>));
```


#### フィールドタイプ null
値がnullの際はこれに該当します。例えば`int[]`で定義した配列を何も初期化せずに渡した場合、サーバー側で`null`タイプとして保管されます。nullを取りうるarrayやreferenceはnullチェックをすべきでしょう。
#### フィールドタイプ timestamp
`timestamp(タイムスタンプ)`は`Timestamp`に変換できます。いきなり`DateTime`や`DateTimeOffset`には変換できません。キャストエラーになります。これらにしたい際は、`Timestamp`に変換後に、`.ToDateTime()`などを用いてあげましょう。

```.cs
(Timestamp) Convert.ChangeType(docDictionary["path"], typeof(Timestamp));
```

#### フィールドタイプ geopoint
`geopoint(地理位置情報)`は`GeoPoint`に変換できます。

```.cs
(GeoPoint) Convert.ChangeType(docDictionary["path"], typeof(GeoPoint));
```

#### フィールドタイプ reference
- referenceのみ調査中です。