建立資料庫結構

本指南將介紹資料架構中的部分重要概念,以及在 Firebase Realtime Database 中建構 JSON 資料的最佳做法。

建構結構良好的資料庫需要相當多的前置作業。最重要的是,您必須規劃資料的儲存方式,並在日後擷取資料時盡可能簡化這項程序。

資料的結構:JSON 樹狀結構

所有 Firebase Realtime Database 資料都會儲存為 JSON 物件,您可以將資料庫想像成雲端託管 JSON 樹狀結構。與 SQL 資料庫不同的是,這個資料庫中沒有表格或記錄。將資料新增至 JSON 樹狀結構時,資料會在現有 JSON 結構中變成一個節點,且包含關聯的金鑰。您可以提供自己的金鑰,例如使用者 ID 或語意名稱,也可以使用 push() 為您提供金鑰。

如果您自行建立索引鍵,則必須採用 UTF-8 編碼,最多可達 768 個位元組,且不得包含 .$#[]/ 或 ASCII 控制字元 0-31 或 127。您也不能在值中使用 ASCII 控制字元。

舉例來說,假設某個即時通訊應用程式可讓使用者儲存基本個人資料和聯絡人清單。一般使用者設定檔位於 /users/$uid 等路徑中。使用者 alovelace 的資料庫項目可能會如下所示:

{
  "users": {
    "alovelace": {
      "name": "Ada Lovelace",
      "contacts": { "ghopper": true },
    },
    "ghopper": { ... },
    "eclarke": { ... }
  }
}

雖然資料庫使用 JSON 樹狀結構,但儲存在資料庫中的資料可做為特定原生類型表示,對應至可用的 JSON 類型,協助您編寫更易於維護的程式碼。

資料結構的最佳做法

避免巢狀資料

由於 Firebase Realtime Database 允許巢狀資料的深度達到 32 個層級,您可能會認為這應該是預設結構。不過,當您在資料庫中擷取資料時,也會擷取所有子節點。此外,當您授予使用者資料庫中某個節點的讀取或寫入權限時,也會授予該使用者該節點下所有資料的存取權。因此,在實際操作中,建議您盡可能讓資料結構保持平面。

舉例來說,請考量下列多重巢狀結構,瞭解為何巢狀資料不好:

{
  // This is a poorly nested data architecture, because iterating the children
  // of the "chats" node to get a list of conversation titles requires
  // potentially downloading hundreds of megabytes of messages
  "chats": {
    "one": {
      "title": "Historical Tech Pioneers",
      "messages": {
        "m1": { "sender": "ghopper", "message": "Relay malfunction found. Cause: moth." },
        "m2": { ... },
        // a very long list of messages
      }
    },
    "two": { ... }
  }
}

在這種巢狀設計中,資料的迴迭會變得有問題。舉例來說,如果要列出聊天對話的標題,就必須將整個 chats 樹狀結構 (包括所有成員和訊息) 下載到用戶端。

展開資料結構

如果資料分割為個別路徑 (也稱為去標準化),則可視需要透過個別呼叫,有效下載資料。請考慮這個扁平化結構:

{
  // Chats contains only meta info about each conversation
  // stored under the chats's unique ID
  "chats": {
    "one": {
      "title": "Historical Tech Pioneers",
      "lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
      "timestamp": 1459361875666
    },
    "two": { ... },
    "three": { ... }
  },

  // Conversation members are easily accessible
  // and stored by chat conversation ID
  "members": {
    // we'll talk about indices like this below
    "one": {
      "ghopper": true,
      "alovelace": true,
      "eclarke": true
    },
    "two": { ... },
    "three": { ... }
  },

  // Messages are separate from data we may want to iterate quickly
  // but still easily paginated and queried, and organized by chat
  // conversation ID
  "messages": {
    "one": {
      "m1": {
        "name": "eclarke",
        "message": "The relay seems to be malfunctioning.",
        "timestamp": 1459361875337
      },
      "m2": { ... },
      "m3": { ... }
    },
    "two": { ... },
    "three": { ... }
  }
}

您現在可以透過每個對話只下載幾個位元組的方式,遍歷聊天室清單,快速擷取用於在 UI 中列出或顯示聊天室的結構描述資料。您可以個別擷取訊息,並在訊息傳送時顯示,讓 UI 保持快速回應。

建立可調整的資料

建構應用程式時,通常建議下載清單的子集。如果清單含有數千筆記錄,這種情況就特別常見。如果這項關係是靜態且單向的,您可以直接將子項物件巢狀結構納入父項。

有時這類關係會更具動態性,或者可能需要將這類資料去規格化。您通常可以使用查詢來擷取資料的子集,藉此將資料去除規格化,如「擷取資料」一文所述。

但這可能仍不足以解決問題。舉例來說,使用者和群組之間的關係是雙向的。使用者可以屬於群組,而群組則包含使用者清單。決定使用者屬於哪些群組時,情況就會變得複雜。

您需要以簡單的方式列出使用者所屬的群組,並只擷取這些群組的資料。群組的索引在這方面可以提供很大的幫助:

// An index to track Ada's memberships
{
  "users": {
    "alovelace": {
      "name": "Ada Lovelace",
      // Index Ada's groups in her profile
      "groups": {
         // the value here doesn't matter, just that the key exists
         "techpioneers": true,
         "womentechmakers": true
      }
    },
    ...
  },
  "groups": {
    "techpioneers": {
      "name": "Historical Tech Pioneers",
      "members": {
        "alovelace": true,
        "ghopper": true,
        "eclarke": true
      }
    },
    ...
  }
}

您可能會發現,這會在 Ada 的記錄和群組下方儲存關係,導致部分資料重複。alovelace 現已編入群組索引,techpioneers 則列在 Ada 的個人資料中。因此,如要從群組中刪除 Ada,必須在兩個地方更新。

這是雙向關係的必要備援機制。這可讓您快速且有效率地擷取 Ada 的會員資格,即使使用者或群組清單規模達到數百萬,或是 Realtime Database 安全性規則阻止存取部分記錄,也能正常運作。

這種方法會將 ID 列為鍵並將值設為 true,藉此反轉資料,讓檢查鍵的操作變得簡單,就像讀取 /users/$uid/groups/$group_id 並檢查是否為 null 一樣。索引比查詢或掃描資料更快、更有效率。

後續步驟