建立資料庫結構

本指南涵蓋資料架構的一些重要概念,以及在 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 一樣簡單。與查詢或掃描資料相比,索引的速度更快,效率也高出許多。

後續步驟