Cấu trúc cơ sở dữ liệu của bạn

Hướng dẫn này trình bày một số khái niệm chính trong kiến trúc dữ liệu và các phương pháp hay nhất để cấu trúc dữ liệu JSON trong Firebase Realtime Database.

Việc xây dựng một cơ sở dữ liệu có cấu trúc đúng cách đòi hỏi bạn phải suy nghĩ trước khá nhiều. Quan trọng nhất là bạn cần lên kế hoạch về cách lưu và sau đó truy xuất dữ liệu để giúp quá trình đó diễn ra dễ dàng nhất có thể.

Cách cấu trúc dữ liệu: đó là một cây JSON

Tất cả dữ liệu Firebase Realtime Database đều được lưu trữ dưới dạng đối tượng JSON. Bạn có thể coi cơ sở dữ liệu này là một cây JSON được lưu trữ trên đám mây. Không giống như cơ sở dữ liệu SQL, cơ sở dữ liệu này không có bảng hoặc bản ghi. Khi bạn thêm dữ liệu vào cây JSON, dữ liệu đó sẽ trở thành một nút trong cấu trúc JSON hiện có với một khoá được liên kết. Bạn có thể cung cấp khoá của riêng mình, chẳng hạn như mã người dùng hoặc tên ngữ nghĩa, hoặc hệ thống có thể cung cấp khoá cho bạn bằng cách sử dụng push().

Ví dụ: hãy xem xét một ứng dụng trò chuyện cho phép người dùng lưu trữ hồ sơ cơ bản và danh sách liên hệ. Hồ sơ người dùng thông thường nằm ở một đường dẫn, chẳng hạn như /users/$uid. Người dùng alovelace có thể có một mục nhập cơ sở dữ liệu trông như sau:

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

Mặc dù cơ sở dữ liệu sử dụng cây JSON, nhưng dữ liệu được lưu trữ trong cơ sở dữ liệu có thể được biểu diễn dưới dạng một số loại gốc tương ứng với các loại JSON có sẵn để giúp bạn viết mã dễ bảo trì hơn.

Các phương pháp hay nhất cho cấu trúc dữ liệu

Tránh lồng dữ liệu

Firebase Realtime Database cho phép lồng dữ liệu tối đa 32 cấp, bạn có thể nghĩ rằng đây là cấu trúc mặc định. Tuy nhiên, khi tìm nạp dữ liệu tại một vị trí trong cơ sở dữ liệu, bạn cũng sẽ truy xuất tất cả các nút con của dữ liệu đó. Ngoài ra, khi cấp cho ai đó quyền đọc hoặc ghi tại một nút trong cơ sở dữ liệu, bạn cũng cấp cho họ quyền truy cập vào tất cả dữ liệu trong nút đó. Do đó, trên thực tế, tốt nhất là bạn nên giữ cho cấu trúc dữ liệu của mình càng đơn giản càng tốt.

Để biết ví dụ về lý do dữ liệu lồng nhau không tốt, hãy xem xét cấu trúc lồng nhau nhiều lần sau:

{
  // 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": { "..." }
  }
}

Với thiết kế lồng nhau này, việc lặp lại dữ liệu sẽ trở nên có vấn đề. Ví dụ: để liệt kê tiêu đề của các cuộc trò chuyện, bạn cần tải toàn bộ cây chats xuống máy khách, bao gồm tất cả thành viên và tin nhắn.

Làm phẳng cấu trúc dữ liệu

Nếu dữ liệu được chia thành các đường dẫn riêng biệt (còn gọi là khử chuẩn hoá), thì dữ liệu đó có thể được tải xuống hiệu quả trong các lệnh gọi riêng biệt, khi cần. Hãy xem xét cấu trúc được làm phẳng này:

{
  // 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": { "..." }
  }
}

Giờ đây, bạn có thể lặp lại danh sách các phòng bằng cách chỉ tải xuống một vài byte cho mỗi cuộc trò chuyện, nhanh chóng tìm nạp siêu dữ liệu để liệt kê hoặc hiển thị các phòng trong giao diện người dùng. Bạn có thể tìm nạp tin nhắn riêng biệt và hiển thị khi tin nhắn đến, cho phép giao diện người dùng luôn phản hồi nhanh chóng.

Tạo dữ liệu có thể mở rộng

Khi xây dựng ứng dụng, thường thì bạn nên tải xuống một tập hợp con của danh sách. Điều này đặc biệt phổ biến nếu danh sách chứa hàng nghìn bản ghi. Khi mối quan hệ này là tĩnh và một chiều, bạn chỉ cần lồng các đối tượng con vào đối tượng mẹ.

Đôi khi, mối quan hệ này mang tính động hơn hoặc bạn có thể cần khử chuẩn hoá dữ liệu này. Nhiều lần, bạn có thể khử chuẩn hoá dữ liệu bằng cách sử dụng một truy vấn để truy xuất một tập hợp con của dữ liệu, như đã thảo luận trong phần Truy xuất dữ liệu.

Tuy nhiên, ngay cả điều này cũng có thể không đủ. Ví dụ: hãy xem xét mối quan hệ hai chiều giữa người dùng và nhóm. Người dùng có thể thuộc một nhóm và các nhóm bao gồm danh sách người dùng. Khi đến lúc quyết định người dùng thuộc nhóm nào, mọi thứ sẽ trở nên phức tạp.

Điều cần thiết là một cách thức hiệu quả để liệt kê các nhóm mà người dùng thuộc về và chỉ tìm nạp dữ liệu cho những nhóm đó. Một chỉ mục của các nhóm có thể giúp ích rất nhiều trong trường hợp này:

// 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
      }
    },
    // ...
  }
}

Bạn có thể nhận thấy rằng điều này sẽ sao chép một số dữ liệu bằng cách lưu trữ mối quan hệ trong cả bản ghi của Ada và trong nhóm. Giờ đây, alovelace được lập chỉ mục trong một nhóm và techpioneers được liệt kê trong hồ sơ của Ada. Vì vậy, để xoá Ada khỏi nhóm, bạn phải cập nhật ở hai nơi.

Đây là một sự dư thừa cần thiết cho mối quan hệ hai chiều. Điều này cho phép bạn tìm nạp tư cách thành viên của Ada một cách nhanh chóng và hiệu quả, ngay cả khi danh sách người dùng hoặc nhóm mở rộng lên đến hàng triệu hoặc khi các quy tắc bảo mật Realtime Database ngăn chặn quyền truy cập vào một số bản ghi.

Phương pháp này (đảo ngược dữ liệu bằng cách liệt kê mã nhận dạng dưới dạng khoá và đặt giá trị thành true) giúp việc kiểm tra khoá trở nên đơn giản như đọc /users/$uid/groups/$group_id và kiểm tra xem khoá đó có phải là null hay không. Chỉ mục nhanh hơn và hiệu quả hơn nhiều so với việc truy vấn hoặc quét dữ liệu.

Các bước tiếp theo