建立資料庫結構

本指南說明資料架構的一些重要概念,以及建構 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,就必須在兩個位置更新

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

這種方法會藉由將 ID 列出為鍵並將值設為 true 來反轉資料,使檢查鍵就像讀取 /users/$uid/groups/$group_id,並檢查其是否為 null 一樣簡單。相較於查詢或掃描資料,索引速度更快,也更有效率。

後續步驟