Firestoreの特徴とアーキテクチャを公開するっぴ

17086 ワード

TL;DR

リレーショナルデータベースに比べて使いずらいと言われるfirestoreのアーキテクチャと勘所を公開するっぴ。

ドキュメントDBの克服の仕方

firestoreの特徴の1つ目がドキュメントデータベースである点です。リレーショナルデータベースの場合、2次元の表で表現されるため、同じフィールドのデータ型は必ず同一になります。

フィールド データ型
name String
age int

ドキュメントデータベースであるfirestoreでは型の混在が可能です。下の例では1つめのageがint型なのに対して、2つめのageは文字列となっています。SDKをそのまま使用するとMap型で提供されるため事故が発生しやすくなります。

sample.json
[
{
  "name": "sarukun"
  "age": 20
},
{
  "name": "sarukun"
  "age": "20"
},
]

この問題に対しては下記のようなEntityクラスを使用することで解決できます。

user.dart
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
  }
}

この状態で後からユーザステータスを追加する場合はコンストラクタでデフォルトの値を埋め込むことで、プログラムからは気にせずに扱えます。

user.dart
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

user.dart
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

user.dart
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の防ぎ方と同様です。

sample.dart
final itemMap = <String, Item>{};
<UserFavorite>[].map((e) => e.copyWith(item: itemMap[e.itemId]))
item.dart
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を使用し負荷を下げています。