建立資料庫結構

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

建構結構良好的資料庫需要深思熟慮。最重要的是,您必須規劃資料的儲存和擷取方式,盡可能簡化這個程序。

資料結構:JSON 樹狀結構

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

舉例來說,假設某個即時通訊應用程式允許使用者儲存基本個人資料和聯絡人清單。一般使用者個人資料位於 /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 中列出或顯示聊天室。訊息可以分開擷取,並在送達時顯示,讓使用者介面保持快速回應。

建立可擴充的資料

建構應用程式時,通常最好下載清單的子集。如果清單包含數千筆記錄,這種情況尤其常見。如果這種關係是靜態且單向的,只要將子項物件巢狀結構置於父項物件下方即可。

有時,這種關係會更動態,或者可能需要反正規化這項資料。在許多情況下,您可以透過查詢擷取資料子集,藉此反正規化資料,詳情請參閱「擷取資料」。

但即使如此,可能還是不夠。舉例來說,假設使用者和群組之間存在雙向關係。使用者可以屬於某個群組,而群組則包含使用者清單。決定使用者所屬群組時,情況會變得複雜。

我們需要一種簡潔的方法,列出使用者所屬的群組,並只擷取這些群組的資料。群組索引在這裡可以提供很大的幫助:

// 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,即可輕鬆檢查鍵。索引比查詢或掃描資料更快,也更有效率。

後續步驟