くらげになりたい。

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

AngularFirebaseのNoSQL設計パターンをER図で書いてみた

前回の記事と同様に以下の記事の内容を整理した。

出てくる例ごとにER図を書いてみると、なんとなくNoSQLわかってきたかも。

基本的な考え方

  • 複製
    • 複数のDocumentを作成しないよう、あらかじめ展開しておく
    • ただし、元データが更新された時は、複数のDocumentで更新が必要
  • 集約
    • 集計処理は、Firestoreのクラウド機能をつかってサーバ側でおこなう
  • 複合キー
    • 単に、userXYZ_postABCのように、組み合わせて一意のIDになるようにする
  • バケット
    • コレクションを単一の文書に分割する複製/集約の形式
    • 参照したい単位で別途コレクションを作成する。
  • グループ コレクション
    • 所属しているグループIDがキーとなってる親ドキュメントをもたせる
    • 所属が更新するたびに、グループIDが一致するキーのフラグを更新する

例1: ショッピングカート

概要

よくあるショッピングカートの例

  • One-to-One: ユーザのカート
  • Many-to-Many: ユーザが商品をカートに入れる
  • One-to-Many: ユーザが商品を注文

また、商品には在庫数などがある

ER図

f:id:wannabe-jellyfish:20190521020415p:plain

データモデル
products/{productID} ... 商品のマスタ
  - productInfo (any)
  -- amountInStock (number) ... 商品の在庫情報

users/{userID}
  -- userInfo (any)  ... ユーザの基本情報
  ++ orders          ... ユーザの注文(1対多)
     -- items [      ... 注文した商品
       { product: productID, qty: 23 }
     ]

carts/{userID}       ... ユーザのカート(1対1)
  -- total (number)  ... 集約
  - products {       ... ユーザがカートに入れた商品(多対多)
      productId: quantity
    }
  ]

例1-1: ショッピングカートのカート部分(多対多)

f:id:wannabe-jellyfish:20190521020501p:plain

products/{productID} ... 商品のマスタ
  - productInfo (any)
  -- amountInStock (number) ... 商品の在庫情報

users/{userID}
  -- userInfo (any)  ... ユーザの基本情報

carts/{userID}       ... ユーザのカート(1対1)
  -- total (number)  ... 集約
  - products {       ... ユーザがカートに入れた商品(多対多)
      productId: quantity
    }
  ]

例1-2: ショッピングカートの注文部分(明細構造)

f:id:wannabe-jellyfish:20190521020650p:plain

products/{productID} ... 商品のマスタ
  - productInfo (any)
  -- amountInStock (number) ... 商品の在庫情報

users/{userID}
  -- userInfo (any)  ... ユーザの基本情報
  ++ orders          ... ユーザの注文(1対多)
     -- items [      ... 注文した商品
       { product: productID, qty: 23 }
     ]

例2: フォロー

Twitterみたいなフォロー/アンフォローの例。

  • 多対多の関係は、followerID_followedIDで表現
  • fieldでそれぞれのIDをもたせて、検索できるようにする
  • 各ユーザの情報は、プログラムJOINする
  • フォロー数とかは、集約フィールドをもたせる

ER図

f:id:wannabe-jellyfish:20190521020808p:plain

データモデル

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


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

クエリ

// ユーザー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);

例3: スレッドコメント/階層ツリー

ブログとかへのスレッド化されたコメントの例

ER図

f:id:wannabe-jellyfish:20190521020828p:plain

データモデル

posts/{postId}                       ... pコメントされる記事
  ++ comments/{commentB}             ... コメント自体
    -- createdAt (date)
    parent: commentA                 ... 親コメントのID
    children: [ commentC, commentD ] ... 子コメントのID一覧

クエリ

// ルートコメントを取得する
comments.where('parent', '==', null)

例4: タグ/ハッシュタグ

Twitterのようなタグ/ハッシュタグの例。

ER図

f:id:wannabe-jellyfish:20190521020848p:plain

データモデル

tweets/{tweetId}         ... ツイート
  -- content (string)
  -- tags: {             ... タグを含むかのフラグ(多対多)
       angular: true,
       firebase: true
  }

tags/{content}           ... タグの一覧
  -- content (string)
  -- tweetCount (number) ... 集約: タグを持つツイート数

クエリ

// 特定のタグのすべてのツイートを取得
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);

以上!!

参考にしたサイト様