Xây dựng cấu trúc cho cơ sở dữ liệu

Hướng dẫn này trình bày một số khái niệm chính về cấu trúc dữ liệu và các phương pháp hay nhất để tạo cấu trúc dữ liệu JSON trong Cơ sở dữ liệu theo thời gian thực của Firebase.

Việc xây dựng cơ sở dữ liệu có cấu trúc hợp lý đòi hỏi bạn phải suy nghĩ khá kỹ lưỡng. Điều quan trọng nhất là bạn cần lập kế hoạch về cách dữ liệu sẽ được lưu và truy xuất sau này để quá trình đó trở nên dễ dàng nhất có thể.

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

Tất cả dữ liệu Cơ sở dữ liệu theo thời gian thực của Firebase đều được lưu trữ dưới dạng đối tượng JSON. Bạn có thể coi cơ sở dữ liệu 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, 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 các khoá của riêng mình, chẳng hạn như mã nhận dạng người dùng hoặc tên ngữ nghĩa, hoặc các khoá này có thể được cung cấp cho bạn bằng phương thức push().

Nếu bạn tạo khoá của riêng mình, thì các khoá đó phải được mã hoá UTF-8, có thể dài tối đa 768 byte và không được chứa ., $, #, [, ], / hay ký tự điều khiển ASCII 0-31 hoặc 127. Bạn cũng không thể sử dụng các ký tự điều khiển ASCII trong chính các giá trị đó.

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 bạ. Một hồ sơ người dùng thông thường nằm tại một đường dẫn, chẳng hạn như /users/$uid. Người dùng alovelace có thể có mục nhập cơ sở dữ liệu giố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 lưu trữ trong cơ sở dữ liệu có thể được biểu thị dưới dạng một số kiểu gốc nhất định 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

Vì Cơ sở dữ liệu theo thời gian thực Firebase cho phép lồng dữ liệu sâu tối đa 32 cấp, nên bạn có thể cho rằng đây nên 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 truy xuất tất cả các nút con của vị trí đó. 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 bạn nên giữ cho cấu trúc dữ liệu càng phẳng càng tốt.

Để biết ví dụ về lý do khiến dữ liệu lồng nhau là không hợp lệ, hãy xem xét cấu trúc lồng nhau theo cấp số nhân sau đây:

{
  // 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 qua dữ liệu sẽ gặp vấn đề. Ví dụ: để liệt kê tiêu đề của các cuộc trò chuyện, bạn phải tải toàn bộ cây chats (bao gồm tất cả thành viên và tin nhắn) xuống ứng dụng.

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

Thay vào đó, nếu dữ liệu được chia thành các đường dẫn riêng biệt, còn gọi là quá trình chuẩn hoá, thì bạn có thể tải dữ liệu xuống một cách 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 phẳng sau:

{
  // 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 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ị phòng trong giao diện người dùng. Tin nhắn có thể được tìm nạp riêng và hiển thị khi đến, cho phép giao diện người dùng luôn phản hồi và nhanh chóng.

Tạo dữ liệu giúp tăng quy mô

Khi tạo ứng dụng, thường thì bạn nên tải một tập hợp nhỏ danh sách xuống. Đ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 trong thành phần mẹ.

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

Nhưng ngay cả điều này cũng có thể chưa đủ. Ví dụ: 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 về một nhóm và nhóm bao gồm một danh sách người dùng. Khi cần xác định người dùng thuộc nhóm nào, mọi thứ trở nên phức tạp.

Việc cần làm là liệt kê các nhóm chứa người dùng và chỉ tìm nạp dữ liệu của các nhóm đó. Chỉ mục nhóm có thể giúp ích rất nhiều tại đâ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 thao tác 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. alovelace hiện đượ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 Ada ở hai vị trí.

Đây là yếu tố dự phòng cần thiết cho mối quan hệ hai chiều. Nhờ giải pháp này, bạn có thể tìm nạp 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 tăng lên đến hàng triệu người hoặc khi các quy tắc bảo mật của Cơ sở dữ liệu theo thời gian thực 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ê các mã nhận dạng là khoá và đặt giá trị thành true, giúp việc kiểm tra khoá cũng đơ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 rất nhiều so với việc truy vấn hoặc quét dữ liệu.

Các bước tiếp theo