Firestoreで多対多のrelationを作るなど
モバイルアプリ作ってみたいと思い、FlutterとFirestoreの素振りを始めました。
Key Value Storeの使い方(というかRDB以外のデータベースほとんど)についてちゃんと考えたことがなかったので、基本っぽいところから結構苦労しました。
やったこと
- KVS (Firestore)で多対多のデータ構造を作る。
- ネストさせたStreamBuilderでhas_manyのさらにhas_manyの情報を全部表示する。
前提
以下のデータ構造を作ることを考えます。(Rails風)
グループで保有する持ち物の管理みたいなイメージです。
group has_many users
user has_many groups
user has_many items
item belongs_to user
RDBの場合、groups、users、itemsのそれぞれに対応するテーブルと、groupとuserの中間テーブルあたりを作ればもう終わりって感じですね。
Firestoreでやる
KVSのメリットの一つはキーを指定すれば子要素も一度に取得できることですが、
素直に多対多の関係を作ろうとすると、同じデータを複数作らないといけなくなりそうです。(非正規化)
色々考えた結果、配列型のフィールドにreferenceを格納する方法に落ち着きました。(KVSのメリットは放棄)
// 特定のgroupに属するuser
CollectionReference users = FirebaseFirestore.instance.collection('users').where('groups', arrayContains: groupDocumentRef)
// 特定のuserが属しているgroup
CollectionReference groups = FirebaseFirestore.instance.collection('groups').where('users', arrayContains: userDocumentRef)
これで相互に参照できます。
データの整合性を保つための処理コストは比較的小さいはず。多分
has_manyのhas_manyを取ってくる
groupAに属するuser全員の持つitemを全て表示したいとき、groupのサブコレクションに情報を持っていれば、groupのキーを一つ指定するだけで取得できますね。(KVSのメリット)
今回はそんな構成にしていないので、違うやり方を使います。
FlutterのWidgetでデータを表示することを考えます。
StreamBuilderをネストさせて実現させました。
まず2個streamを準備します。
Stream<List<DocumentReference>> _getUserIdsStream(DocumentReference groupRef) {
return groupRef
.snapshots()
.map((snapshot) {
List<dynamic> userRefs = snapshot['users'] ?? [];
return userRefs.map((userRef) => (userRef as
DocumentReference)).toList();
});
}
Stream<QuerySnapshot> _getItemsStream(List<DocumentReference> userIds) {
return FirebaseFirestore.instance.collection('items').where('user', whereIn: userIds).snapshots();
}
上記のstreamをStreamBuilderに設定します。
StreamBuilder(
stream: _getUserIdsStream(groupRef),
builder: (context, userIdsStream) {
if (userIdsStream.hasData) {
final userIds = userIdsStream.data!;
return StreamBuilder(
stream: _getItemsStream(userIds),
builder: (context, AsyncSnapshot<QuerySnapshot> streamSnapshot) {
if (streamSnapshot.hasData) {
return ListView.builder(
itemCount: streamSnapshot.data!.docs.length,
itemBuilder: (context, index) {
final DocumentSnapshot documentSnapshot = streamSnapshot.data!.docs[index];
return ListTile(
title: Text(documentSnapshot['name']),
);
}
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
}
return const Center(
child: CircularProgressIndicator(),
);
}
)
まとめ
今回のケースはどちらかというと整合性の方が大事なので大人しくRDBを使っておいた方がよさそうだと思いました。
基本はRDBを使い、特にリアルタイム性が求められる箇所だけなるべくシンプルなデータ構造にした上でFirestoreを使うのがよさそうです。
Discussion