Firestore - Native modeにDatastoreのクライアントライブラリで覗いてみる

📛📛📛最近Firebaseに入門して、WEBアプリ作っています📛📛📛

[Firebaseでチャットアプリを作る日記](https://blog.knightso.co.jp/entry/2019/12/12/013741)

自分はDatastoreやFirestore(Datastore mode)はそれなりに経験がありますが、Firestore(Native mode)は今回初めて触っています。

コレクションやドキュメント、サブコレクションなど、Datastoreとは大分雰囲気が違うなー、と思っていたのですが、触っているうちにだんだん「あれ？やっぱり中身はDatastoreと似てない？」と思い始めました。

試しにFirestore(Native mode)を、Datastoreのクライアントライブラリで覗いたらどうなるだろう？と思いついて、試してみることにしました。
注：推奨はされていないと思います。良い子はマネしないでください😅

# 準備

[こちら](https://blog.knightso.co.jp/entry/2019/12/13/060224)の手順でFirebaseのプロジェクトを用意します。今回はHostingとFirestoreのみ使用します。
Hostingのデプロイまで済ませます。

今回Firestoreのデータ登録は、Hostingされたページのコンソールから直接JavaScriptを入力して行います[^1]

[^1]: こんなに手軽にデータ登録できてしまうのは怖いですね^^; やはり適切なセキュリティルール設定は必須ですね。

# 覗いてみる

## シンプルなドキュメント

コレクション「hoge」に、ドキュメントを一件追加します。

```javascript
firebase.firestore().collection("hoge")
  .add({text: 'test', number: 999, array: ["aaa", "bbb"]});
```

保存されました。
<img width="1275" alt="スクリーンショット 2019-12-21 午後2.53.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/f5040d91-0905-4343-8828-24101db57b75.png">

Datastoreクライアントライブラリで覗いてみます。
言語はGoを使います。

```go
package main

import (
	"context"
	"fmt"
	"log"

	"cloud.google.com/go/datastore"
	"google.golang.org/api/iterator"
)

func main() {
	ctx := context.Background()

	// Set your Google Cloud Platform project ID.
	projectID := "native-ds"

	// Creates a client.
	client, err := datastore.NewClient(ctx, projectID)
	if err != nil {
		log.Fatalf("Failed to create client: %v", err)
	}

	q := datastore.NewQuery("hoge")

	t := client.Run(ctx, q)	for {
		var entity datastore.PropertyList
		key, err := t.Next(&entity)
		if err == iterator.Done {
			break
		}
		if err != nil {
			log.Fatal(err)
		}

		fmt.Printf("\n\nkey:%s\n", key)

		for _, p := range entity {
			fmt.Printf("  %+v\n", p)
		}
	}
}
```

出力↓

```
key:/hoge,OopK2m1wJPg7fCYkRGd3
  {Name:text Value:test NoIndex:true}
  {Name:array Value:[aaa bbb] NoIndex:false}
  {Name:number Value:999 NoIndex:true}
```

コレクションがKind名で、IDがKey名になっています。この辺は結構直感的ですね。

フィールドもそれぞれプロパティとして保存されています。こちらもほぼ直感的ですが、よく見ると殆ど `NoIndex:true` になっていますね。実際フィルター指定しても `no matching index found` というエラーが出ます。arrayフィールドのみなぜかNoIndex:falseとなっているのが不思議ですが、こちらもDatastoreのフィルターは効いていません。

@apstndb さんの↓の仮説があってそうな雰囲気。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Cloud Firestore Native mode は Cloud Firestore Datastore mode の上に構築されているかというと、 entity は上に構築されていても矛盾がなさそうだけど index はなんだか違うなという感じがありますよね</p>&mdash; _ (@apstndb) <a href="https://twitter.com/apstndb/status/1208026107758305281?ref_src=twsrc%5Etfw">December 20, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## サブコレクション

コレクション「hoge」に、ドキュメントを一件追加します。

```javascript
firebase.firestore().collection("hoge")
  .doc("OopK2m1wJPg7fCYkRGd3")
  .collection("fuga")
  .add({text:"fugatest"});
```

保存されました。
<img width="1153" alt="スクリーンショット 2019-12-21 午後3.19.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/f8b1e7da-eee7-536e-e4b2-494591e4b9ac.png">

Datastoreクライアントライブラリで覗きます。
先のGoコードのKind名を `fuga` に変えるだけです。

出力↓

```
key:/hoge,OopK2m1wJPg7fCYkRGd3/fuga,saijWPqk7zwRV0yTKAmi
  {Name:text Value:fugatest NoIndex:true}
```

hogeコレクションが親KeyのKind名、fukaサブコレクションが子KeyのKind名になっていて、ちょうどDatastoreのKeyのパスの階層とマッチしてますね。

ついでに孫も作ります。

```javascript
firebase.firestore().collection("hoge")
  .doc("OopK2m1wJPg7fCYkRGd3")
  .collection("fuga")
  .doc("saijWPqk7zwRV0yTKAmi")
  .collection("piyo")
  .add({text:"piyotest"});
```
  
Datastoreクライアントライブラリ出力

```
key:/hoge,OopK2m1wJPg7fCYkRGd3/fuga,saijWPqk7zwRV0yTKAmi/piyo,O03MQHQMDVV3uo1aH2Nm
{Name:text Value:piyotest NoIndex:true}
```

孫も同様にKeyパス階層に組み込まれました。

グローバルクエリで結果が返ってくるということは、EntitiesByKindテーブルが作られているということでしょうか。
最近サポートされた [Collection group queries](https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query) がDatastoreのグローバルクエリ相当な気がしています。

## いきなり孫を作る

ここまで見てきて、Native modeのドキュメントとDatastore modeのエンティティは内部的にはほぼ同じものではないかと推測できます。

Datastore modeでは親Keyのエンティティが存在しなくても子Keyのエンティティや孫Keyのエンティティを保存することが出来ます。
Native modeでも[Cloud Firestore Data model](https://firebase.google.com/docs/firestore/data-model) に、

> Collections and documents are created implicitly in Cloud Firestore. Simply assign data to a document within a collection. If either the collection or document does not exist, Cloud Firestore creates it.

と記されていますので、試してみます。

```javascript
firebase.firestore().collection("hoge")
  .doc("nodoc")
  .collection("fuga")
  .doc("nodoc")
  .collection("piyo")
  .add({text:"piyotest2"});
```

できました(^^)

<img width="1273" alt="スクリーンショット 2019-12-21 午後4.30.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/49851206-afe3-a099-cb5f-fe5f362b4df1.png">
<img width="1262" alt="スクリーンショット 2019-12-21 午後4.31.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/27bd933d-4805-29fc-3b90-302321f56e95.png">

Datastoreクライアントライブラリで覗いてみます。
piyoに対するクエリも普通に結果が返ってきます。

```
key:/hoge,OopK2m1wJPg7fCYkRGd3/fuga,saijWPqk7zwRV0yTKAmi/piyo,O03MQHQMDVV3uo1aH2Nm
  {Name:text Value:piyotest NoIndex:true}

key:/hoge,nodoc/fuga,nodoc/piyo,QFWX2rjTszwaBUyUfeVA
  {Name:text Value:piyotest2 NoIndex:true}
```

Collection groupクエリも実行してみました。
<img width="638" alt="スクリーンショット 2019-12-21 午後4.44.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31437/04c78457-ab13-bbcf-51d1-0268cb5e998a.png">

同様の結果が返りました。

「Collectionが存在しない場合は暗黙的に作られる」とあり何かかデータが作成されそうな風に読めますが、何の事は無い、単にドキュメントKeyの祖先Keyになっているだけな気がします。

## Kindlessクエリ

いままでに保存した内容をDatastoreの[Kindlessクエリ](https://cloud.google.com/datastore/docs/concepts/queries#kindless_queries)で覗いてみます。KindlessクエリはKindを指定しないで全てのKindにまたがって検索できる機能です。

GoプログラムのKind名を空にするだけです。

出力↓

```
key:/hoge,OopK2m1wJPg7fCYkRGd3
  {Name:text Value:test NoIndex:true}
  {Name:array Value:[aaa bbb] NoIndex:false}
  {Name:number Value:999 NoIndex:true}

key:/hoge,OopK2m1wJPg7fCYkRGd3/fuga,saijWPqk7zwRV0yTKAmi
  {Name:text Value:fugatest NoIndex:true}

key:/hoge,OopK2m1wJPg7fCYkRGd3/fuga,saijWPqk7zwRV0yTKAmi/piyo,O03MQHQMDVV3uo1aH2Nm
  {Name:text Value:piyotest NoIndex:true}

key:/hoge,nodoc/fuga,nodoc/piyo,QFWX2rjTszwaBUyUfeVA
  {Name:text Value:piyotest2 NoIndex:true}
```

登録したドキュメント相当のエンティティが全て出力されました。それ以外のナニカは（少なくともエンティティとしては）保存されていなさそうです。

## おまけ

DatastoreでKind一覧を返す `__kind__` にクエリかけてみました。

出力

```
key:/__kind__,fuga
key:/__kind__,hoge
key:/__kind__,piyo
```

ちなみに `__property__` へのクエリは結果0でした。
## mapフィールド

(2020.01.11追記)

mapフィールドを試してみました。

```javascript
firebase.firestore().collection("maptest")
  .add({text: 'test', map: [aaa: "aaa", bbb: "bbb"]});
```

出力

````
key:/maptest,QQf0nBscdc8QdrBDLKkQ
name:text, value(string):test
name:map, value(*datastore.Entity):&{Key: Properties:[{Name:bbb Value:bbb NoIndex:true} {Name:aaa Value:aaa NoIndex:true}]}
```

Embeded Entity(Keyなし)の形式で保存されました。

# まとめ

旧Cloud DatastoreやFirestore Datastore mode経験者は、Firestore native modeを初めて見てかなり異質なものをイメージしてしまうかもしれません（自分がそうでした）。

ただ蓋を開けてみると中身の構造はかなり共通しているので、勘所を抑えればすんなり理解できるのではないかと思います。

Native modeはリアルタイム更新リスナーや、セキュリティルールなどDatastoreにない革新的な機能も持っているので、これから積極的に使っていきたいと思います。