Firestoreの特徴とアーキテクチャを公開するっぴ
TL;DR
リレーショナルデータベースに比べて使いずらいと言われるfirestoreのアーキテクチャと勘所を公開するっぴ。
ドキュメントDBの克服の仕方
firestoreの特徴の1つ目がドキュメントデータベースである点です。リレーショナルデータベースの場合、2次元の表で表現されるため、同じフィールドのデータ型は必ず同一になります。
フィールド | データ型 |
---|---|
name | String |
age | int |
ドキュメントデータベースであるfirestoreでは型の混在が可能です。下の例では1つめのageがint型なのに対して、2つめのageは文字列となっています。SDKをそのまま使用するとMap型で提供されるため事故が発生しやすくなります。
[
{
"name": "sarukun"
"age": 20
},
{
"name": "sarukun"
"age": "20"
},
]
この問題に対しては下記のようなEntityクラスを使用することで解決できます。
class User {
final String name;
final int age;
User({required this.name, required age});
factory User.fromMap(Map map) => User(
name: map['name'],
age: map['age']
);
Map toMap() => {
'name': name,
'age': age
}
}
この状態で後からユーザステータスを追加する場合はコンストラクタでデフォルトの値を埋め込むことで、プログラムからは気にせずに扱えます。
class User {
final String name;
final int age;
final bool suspended
User({required this.name, required age});
factory User.fromMap(Map map) => User(
name: map['name'],
age: map['age']
suspended: map['suspended'] ?? false
);
Map toMap() => {
'name': name,
'age': age
'suspended': suspended
}
}
しかしsuspendedで絞り込んだ場合に、値が入っていないドキュメントが取得できないため、絞り込む場合は、あらかじめマイグレーションする必要があります。マイグレーションはEntityクラス使って全ドキュメントを保存するだけで簡単に行えます。
サブコレクションの勘所
RDBにはないfiresoreの2つ目の特徴がサブコレクションです。例えばUserと好みの飲み物をRDBで表現すると下記のようになります。
User
フィールド | データ型 |
---|---|
name | String |
UserFavorite
フィールド | データ型 |
---|---|
userId | String |
itemId | int |
Item
フィールド | データ型 |
---|---|
itemId | String |
name | int |
firestoreでこれを扱うには、UserとUserFavoriteの2つのドキュメントを使います。
/Users
class User {
final String name;
User({required this.name});
factory User.fromMap(Map map) => User(
name: map['name']
);
Map toMap() => {
'name': name
}
}
/Users/$uid/Favorites
class UserFavorite {
final String name;
User({required this.name});
factory UserFavorite.fromMap(Map map) => User(
name: map['name']
);
Map toMap() => {
'name': name
}
}
ポイントはFavoriteをUsersのサブコレクションにする点です。こうすることで、あるユーザに属する好きなアイテムをコレクションで表現できます。サブコレクション化することでStreamによる状態監視を行うことができます。
このままだとアイテム名を変更したときに全ユーザのデータを変える必要がありますので、別途Itemテーブルを作りMapで保持し、Favoritesにまぶすことで、nameをマスタデータ化することもできます。この点はRailsのincludesを使ったN+1の防ぎ方と同様です。
final itemMap = <String, Item>{};
<UserFavorite>[].map((e) => e.copyWith(item: itemMap[e.itemId]))
class Item {
final String name;
final String id;
Item({required this.id, required this.name});
factory Item.fromMap(Map map) => User(
name: map['name']
id: map['id']
);
Map toMap() => {
'id': id
'name': name
}
}
データベースアクセス周りはチーム内でライブラリを整備することで意識せずに開発していくことが可能です。Nappsでも社内ORMを使用し負荷を下げています。
Discussion