translate

NoSQL データベース Cloud Firestore でいいねボタンを作ったらデータ構造を何度も変えた話
2021年 1月 26日

Cloud Firestore いいですよね。このサイトでも使っています。

ページ右上に「♥」があると思いますが、そこをクリックすると数字がひとつ増えます。いわゆるいいねボタンです。何度も押せます。この数字を Cloud Firestore で管理していますが、実はデータ構造を何度も変えています。今回はそのお話を。

いいねボタンが作りたかった

このいいねボタンは、ゲームの各コンテンツページと、ブログの各記事ページに設置してあり、それぞれのページ単位で数字を保持しています。

今回作りたかったいいねボタンの仕様は下記の通りです。

  • 各ページ単位でいいねの数を保持したい
  • 全ページのいいねの合算数を取りたい

これを元に Cloud Firestore でデータ構造を考えてみます。

その前に Cloud Firestore の仕組み

structure-data

Cloud Firestore は、クラウドホストの NoSQL データベースです。上記のような コレクションの中にドキュメントがあり、その中にデータ(その中のデータの各項目をフィールドという)があるという、フォルダ構造に似た構造を持つことができます。

その前に Cloud Firestore の課金と通信の仕組み

Cloud Firestore はドキュメントが通信の単位であり、何個のドキュメントを読み書きしたかで課金が発生します。

データ構造設計@1.0.0

最初は下記のような構造を考えました。

keyにはゲームコンテンツのidやブログ記事のidが入っていると思ってください。

like (collection)
  └ count (document)
    ├ key1: 10 (field)
    ├ key2: 20 (field)
    ├ key3: 15 (field)
    └ key4: 30 (field)

like というコレクションに count というドキュメントがあり、その中のデータに各keyのフィールドが入っています。上記のデータ構造のメリットデメリットを考えてみます。

メリット

  • ドキュメントが一つなので通信が1回で済む
  • 全部のデータがひとつのドキュメントに入っているので、合算値が計算できる

デメリット

  • 例えば key が数千個に増えた場合通信量が膨らむ
  • 各keyのデータを個別で取れない
  • ドキュメントには最大容量があるので、無限にデータが入らない

所詮は個人サイトなので、デメリットはないようなものなのですが、そこはエンジニアとして気になってしまい、この案は不採用になりました。

データ構造設計@2.0.0

次はこのようなデータ構造を考えました。

like (collection)
  ├ key1 (document)
  │  └ count: 10 (field)
  ├ key2 (document)
  │  └ count: 20 (field)
  ├ key3 (document)
  │  └ count: 15 (field)
  └ key4 (document)
      └ count: 30 (field)

like というコレクションに各 key のドキュメントがあり、その中に count というフィールドが入っています。では、上記のデータ構造のメリットデメリットを考えてみます。

メリット

  • 各 key がドキュメントになっているので key 単位でいいねの数が取れる
  • ドキュメントの最大容量を考慮しなくてよい
  • データ構造として見やすい

デメリット

  • いいねの合算数を取るために全ドキュメントを取る必要があり key が数千個になった場合、合算数を取るたびに ドキュメントの読み込みが数千回走る
  • よくよく調べたらコレクションに対する秒間の書き込みが500回だったので、いいねを押される回数に制限があった

こちらもデメリットが気になったので、もう少しデータ構造をひねってみました。

データ構造設計@2.1.0

like (collection)
  ├ key1 (document)
  │  └ count: 10 (field)
  ├ key2 (document)
  │  └ count: 20 (field)
  ├ key3 (document)
  │  └ count: 15 (field)
  └ key4 (document)
      └ count: 30 (field)

likeAll (collection)
  └ all (document)
      └ count: 75 (field)

@2.0.0 とほぼ同じですが likeAll というコレクションを作り、そこにいいねの合算数を入れました。 Cloud Firestore 慣れないと冗長に思えますが、冗長はひとつのベストプラクティスです。では、こちらのデータ構造のメリットデメリットを考えてみます。

メリット

  • 各 key がドキュメントになっているので key 単位でいいねの数が取れる
  • ドキュメントの最大容量を考慮しなくてよい
  • データ構造として見やすい
  • いいねの合算数が一回の通信で取れる NEW!!

デメリット

  • いいねの合算数を取るために全ドキュメントを取る必要があり key が数千個になった場合、合算数を取るたびに ドキュメントの読み込みが数千回走る
  • よくよく調べたらコレクションに対する秒間の書き込みが500回だったので、瞬間的にいいねを押される回数に制限があった

メリットがひとつ増えて、デメリットがひとつ消えました。しかしこれでもエンジニアとしてちょっと気になってしまいます。もう少し考えてみます。

データ構造設計@3.0.0

contents (collection)
  ├ key1 (document)
  │  └ like (collection)
  │    └ key1 (document)
  │      └ count: 10 (field)
  ├ key2 (document)
  │  └ like (collection)
  │    └ key2 (document)
  │      └ count: 20 (field)
  ├ key3 (document)
  │  └ like (collection)
  │    └ key3 (document)
  │      └ count: 15 (field)
  └ key4 (document)
      └ like (collection)
        └ key4 (document)
          └ count: 30 (field)

likeAll (collection)
  └ all (document)
      └ count: 75 (field)

contents というコレクションに各 key のドキュメントがあり、その中に like というコレクションが入っていて、その中に各 key のドキュメントがあり、そしてその中に count というフィールドが入っています。

コレクションの中のドキュメントの中にまたコレクションが入っていますが、これはサブコレクションといいます。

このデータ構造のメリットデメリットを見てみます。

メリット

  • 各 key がドキュメントになっているので key 単位でいいねの数が取れる
  • ドキュメントの最大容量を考慮しなくてよい
  • データ構造として見やすい
  • いいねの合算数が一回の通信で取れる
  • サブコレクションを使用したので秒間の書き込み回数が大幅に緩和された NEW!!
  • コレクショングループクエリを使用すれば like というコレクションを一気に取ってこれる NEW!!

デメリット

  • いいねの合算数を取るために全ドキュメントを取る必要があり key が数千個になった場合、合算数を取るたびに ドキュメントの読み込みが数千回走る
  • よくよく調べたらコレクションに対する秒間の書き込みが500回だったので、瞬間的にいいねを押される回数に制限があった

デメリットが消えました(思いつく限りは)。ということでこのサイトのいいねのデータ構造はこれになっています。

おわりに

Cloud Firestore 昔から使っていますが、まだまだ使いこなせている感は自分の中では出ません。もっと勉強しようと思います。

※ もっとこういうデータ構造のほうがいいよ!とあれば、誰か教えてくださいませ。