くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

NoSQLのテクニックが書いてある「Advanced Data Modeling With Firestore by Example」を翻訳してみた

オリジナルの記事は、こちら。

NoSQLのデータ構造や設計について、いろんな事例が書いてあってすてきだったので、
Google翻訳で翻訳した(´ω`) 翻訳精度すごい。。

内容の整理は別途やる予定。


Advanced Techniques in NoSQL

Duplication: 複製

データ複製は、複数のDocumentを参照する必要性を排除するための非常に一般的な手法です。

簡単な例
ユーザードキュメントに対して二次クエリを行わないようにするために、
すべてのツイートドキュメントにユーザー名を複製または埋め込むことがあります。
または、ユーザープロフィールに表示するために、ユーザーDocumentに最近の20件のツイートを複製することもあります。
この方法では、読み取りは速くなりますが、書き込みは遅くなります。

考えること
1つのDocumentですべてのデータを読み取ることができますが、
埋め込まれたデータが変更されたときに複数のDocumentを更新する必要があるかもしれません。

Aggregation: 集約

データ集約とは、データの集まりを分析し、その結果を他のDocumentに保存するプロセスです。
最も簡単な例は、コレクション内の総Document数の保存です。
通常、Firestoreの集約クラウド機能を介してサーバー側で行われます。

Composite Keys: 複合キー

複合キーは、単にuserXYZ_postABCのように、2つ以上の一意のドキュメントIDを組み合わせたものです。
これは、2つのDocument間に一意の関係を強制できるため、非正規化構造の関係をモデル化するのに特に役立ちます。

Bucketing: バケッティング

バケットは、コレクションを単一の文書に分割する複製/集約の形式です。
例としてTwitterを使用して、ツイートのコレクションがあるが、特定のユーザーのツイートを月ごとにまとめたいとしましょう。
これは、ある月のツイートを非常に効率的に読むことを可能にしますが、
欠点は、元のDocumentが更新されてもすべてのデータが同期されるようにするための追加の更新が必要です。

tweets/{tweetId}
  tweetData (any)

februaryTweets/{userId}
  userId
  tweets [
    { tweetData }
  ]

Sharding: シャーディング

多くのNoSQLデータベースでは、スケール変更する必要があります。
シャーディングとは、パフォーマンスを向上させるためにデータベースをより小さな部分に分割する(水平分割)プロセスです。

Firestoreでは、分割は自動的に処理されます。
シャーディングを制御する必要がある唯一のシナリオは、1秒未満の間隔で多数の書き込み操作が一貫して発生している場合です。
Selena Gomezからの新しいツイートで、likeカウントを更新するための計算要件を想像してください。

Pipelining(Unique Firebase Feature): パイプライン化(独自のFirebase機能)

Firebase SDKのもう1つの優れた機能は、Frank van Puffelenが説明しているように
パイプラインと呼ばれるブロックされない方法で読み取り要求を行うことができることです。
Firestoreに問い合わせるとき、応答Aが要求Bを送信するのを待つ必要はありません。
すべての要求を個別に送信することができ、Firebaseは準備ができ次第データで応答します。

パイプライン化はデータ構造化手法ではありませんが、それが私たちの意思決定プロセスを推進します。

一連のドキュメントIDがあるとしましょう。
IDをループして子コンポーネントからドキュメントの読み取りを実行することで、子コンポーネントからの各要求をパイプライン処理できます。
つまり、afs.doc('items /' + id)です。 要求はブロックされないため、
このようにアプリケーションを構築してもパフォーマンスに大きな影響はありません。

<parent-comp>
  <child-comp *ngFor="let id of documentIds">
    <!-- afs.doc('items/' + id) -->
  </child-comp>

</parent-comp>

Group Collection Query: グループコレクションクエリ

グループコレクションクエリは、親の所有者全体で共通のサブコレクションをクエリするときに発生します。
たとえば、Angularカテゴリの投稿を書いたすべてのユーザーに対してブログ投稿を取得することができます。

サブコレクションを介してこのクエリを実行することはできません。
簡単な解決策は投稿をルートコレクションに非正規化することですが、それが不可能な場合は、ここでのプランB…

まず、複製データを親に埋め込みます。
サブコレクションに新しい投稿が作成されると、そのカテゴリがキーとなっている親ドキュメントのcategoriesUsedオブジェクトが更新されます。

users
  - userData (any)
  - categoriesUsed {
     angular: true
     vuejs: true
   }
  ++ posts/{postID}
    -- content
    -- category: angular

これは、以下のように問い合わせます。

users.where('categoriesUsed.angular', '==', true);

この時点で、Angularに投稿したすべてのユーザーがいるので、各ユーザーの投稿サブコレクションを個別にクエリして、
データクライアント側に参加させることができます。

Shopping Cart + Ecommerce NoSQL Model: ショッピングカート+ eコマースNoSQLモデル

eコマースソリューションを構築することは簡単な作業ではありません - それはSQLとNoSQLの両方に当てはまります。
この例では、基本的な在庫管理を使用してショッピングカートをモデル化する方法を説明します。
覚えておいて、あなたは支払い、返品など、他の懸念を持っている可能性があります。

  • One-to-One: ユーザのカート
  • Many-to-Many: ユーザーの商品(カート経由)
  • One-to-Many: ユーザの注文

ユーザーがゲストとしてチェックアウトできるときは、ショッピングカートはFirebase匿名認証とうまく連携します。

Data Model: データモデル

課題は次のとおりです。

プロダクトの価格は変わる可能性があり、カートに反映されるべきです。
プロダクト在庫は限られています。

products/{productID}
  -ductInfo (any)
  -- amountInStock (number)

users/{userID}
  -- userInfo (any)
  ++ orders
     -- items [
       { product: productID, qty: 23 }
     ]

carts/{userID}
  -- total (number)
  -ducts {
      productId: quantity
    }
  ]

ユーザー(ルートコレクション):基本ユーザーデータ

商品(ルートコレクション):商品データと現在の在庫。

カート(ルートコレクション):いずれかのコレクションのドキュメントにuserID === cartIDを設定することで、1対1の関係が作成されます。
ユーザが複数のカートを持っている必要がないため、注文が出されると、カートのデータを消去できます。

注文(ユーザーサブコレクション):注文が作成および確認されたら、クラウド機能を実行して製品の入手可能性を減らすことができます。

User Follow/Unfollow System

当然のことながら、ここでの例としてTwitterを使用しましょう。
複合キーを利用して、独自のコレクションで関係を管理できます。
followerID_followedIDの一意のIDを使用することは、userFoouserBarの後に続くというようなものです。

Data Model: データモデル

users/{userID}
  -- userInfo (any)
  -- followerCount (number)
  -- followedCount (number)


relationships/{followerID_followedID}
  -- followerId (string)
  -- followedId (string)
  -- createdAt  (timestamp)

ユーザー(ルートコレクション):集計されたフォロー数を含む基本ユーザーデータ。

リレーションシップ(ルートコレクション):これはSQLの中間テーブルと同様に機能し、複合キーを使用して各ユーザーとユーザーの関係に一意性を強制します。

Sample Queries: サンプルクエリ

ユーザーAがユーザーBをフォロー:

db.collection('relationships').doc(`${followerId}_${followedId}`);

ユーザーの最新のフォロワー50人を取得:

db.collection('relationships')
  .where('followedId', '==', userId)
  .orderBy('createdAt', 'desc')
  .limit(50);

フォローされているすべてのユーザーを取得:

db.collection('relationships').where('followerId', '==', userId);

Threaded Comments or Hierarchy Tree Structure: スレッド化されたコメントまたは階層ツリー構造

複数のレベルにまたがるツリー構造をモデル化する方法を見てみましょう。
HackerNewsやサブカテゴリのディレクトリに関するスレッド化されたコメントについて考えてみましょう。
実際、私はFirebase RTDBでホストされているHacker Newsの後にデータをモデル化しています。

この実装は、Firebaseで パイプライン処理 を利用するように設計されています -
単一のコレクションを要求する代わりに、大量のドキュメントを個別に要求します。

将来的には、Firestoreは一連のドキュメントIDに基づくクエリをサポートする可能性があります

Data Model: データモデル

posts/{postId}
  ++ comments/{commentB}
    -- createdAt (date)
    parent: commentA
    children: [ commentC, commentD ]

Recursive Query Structure in Angular: Angularの再帰クエリ構造

まずcomments.where('parent', '==', null)でルートコメントを問い合わせることから始めて、それからそれらを私たちのコメントコンポーネントに渡します。

<app-comment *ngFor="let comment of comments | async"
              [commentId]="comment.id">
</app-comment>

これにより、ツリーを描画するために再帰的なコンポーネント(自分自身を呼び出すコンポーネント)を構築することができます。
つまり、コメント文書には子がありますが、スレッド化されたコメントツリーの新しいブランチは引き続きレンダリングされます。
子供をロードする前に「返信を見る」ボタンを追加することで各ブランチを遅延ロードするのも簡単です。

Tags or HashTags: タグまたはハッシュタグ

Twitterのテーマに戻り、Firestoreでハッシュタグをモデル化しましょう。
ハッシュタグは単一の連続した文字列でなければならず、大文字と小文字を区別しないでください。
つまり、タグ自体をドキュメントIDとして使用できます。
たとえば、#AngularFireハッシュタグは、angularfireのドキュメントIDを持ちます。

新しいツイートが作成されるたびに、クラウド関数を使用してタグドキュメントのpostCountプロパティを集計できます。

Data Model

tweets/{tweetId}
  -- content (string)
  -- tags: {
       angular: true,
       firebase: true
  }

tags/{content}
  -- content (string)
  -- tweetCount (number)

Sample Queries

特定のタグのすべてのツイートを取得:

db.collection('tweets').where('tags.angular', '==', true);
// Or
db.collection('tweets').orderBy('tags.angular');

最も人気のあるタグを取得:

db.collection('tags').orderBy('tweetCount', 'desc');

特定のタグを取得:

tagId = 'SomeCoolTag'.toLowerCase();
db.doc('tags/' + tagId);

The End: おわりに

データモデリングが大好きです。 あなた自身の複雑なデータ構造の課題について話したいのなら、Slackに連絡してください。
将来的には、私はこれらのモデルのいくつかを取り、あなたがどんなAngularアプリにでも入れることができる全体の機能を構築するつもりです。