Đọc và ghi dữ liệu

(Không bắt buộc) Tạo nguyên mẫu và thử nghiệm bằng Bộ mô phỏng Firebase

Trước khi nói về cách ứng dụng đọc và ghi vào Cơ sở dữ liệu theo thời gian thực, thì giờ hãy giới thiệu một bộ công cụ mà bạn có thể dùng để tạo nguyên mẫu và thử nghiệm Cơ sở dữ liệu theo thời gian thực chức năng: Bộ mô phỏng Firebase. Nếu bạn đang thử dùng dữ liệu khác mô hình, tối ưu hoá quy tắc bảo mật hoặc nỗ lực tìm ra cách tiết kiệm chi phí để tương tác với hệ thống phụ trợ, nhờ đó có thể làm việc tại địa phương mà không triển khai dịch vụ trực tiếp có thể là ý tưởng hay.

Trình mô phỏng Cơ sở dữ liệu theo thời gian thực là một phần của Bộ trình mô phỏng. Trình mô phỏng này cho phép ứng dụng của bạn tương tác với cấu hình và nội dung cơ sở dữ liệu được mô phỏng, như cũng như các tài nguyên dự án được mô phỏng (không bắt buộc) (các hàm, cơ sở dữ liệu khác, và quy tắc bảo mật).emulator_suite_short

Việc sử dụng trình mô phỏng Cơ sở dữ liệu theo thời gian thực chỉ bao gồm một vài bước:

  1. Thêm một dòng mã vào cấu hình kiểm thử của ứng dụng để kết nối với trình mô phỏng.
  2. Trên gốc của thư mục dự án cục bộ, chạy firebase emulators:start.
  3. Gọi điện từ mã nguyên mẫu của ứng dụng bằng nền tảng Cơ sở dữ liệu theo thời gian thực SDK như thường lệ hoặc sử dụng API REST của cơ sở dữ liệu theo thời gian thực.

Bạn có thể tham khảo hướng dẫn chi tiết về Cơ sở dữ liệu theo thời gian thực và Hàm đám mây. Bạn cũng nên xem Giới thiệu về Bộ mô phỏng.

Nhận một DatabaseReference

Để đọc hoặc ghi dữ liệu từ cơ sở dữ liệu, bạn cần có một thực thể của DatabaseReference:

DatabaseReference ref = FirebaseDatabase.instance.ref();

Ghi dữ liệu

Tài liệu này trình bày các kiến thức cơ bản về cách đọc và ghi dữ liệu Firebase.

Dữ liệu Firebase được ghi vào DatabaseReference và truy xuất bằng đang chờ hoặc lắng nghe các sự kiện do tham chiếu đưa ra. Các sự kiện được phát ra một lần cho trạng thái ban đầu của dữ liệu và lặp lại một lần nữa bất cứ khi nào dữ liệu thay đổi.

Thao tác ghi cơ bản

Đối với các thao tác ghi cơ bản, bạn có thể sử dụng set() để lưu dữ liệu vào một thuộc tính cụ thể thay thế mọi dữ liệu hiện có tại đường dẫn đó. Bạn có thể đặt một tham chiếu thành các loại sau: String, boolean, int, double, Map, List.

Ví dụ: bạn có thể thêm người dùng có set() như sau:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

await ref.set({
  "name": "John",
  "age": 18,
  "address": {
    "line1": "100 Mountain View"
  }
});

Việc sử dụng set() theo cách này sẽ ghi đè dữ liệu tại vị trí được chỉ định, bao gồm mọi nút con. Tuy nhiên, bạn vẫn có thể cập nhật tài khoản của trẻ mà không cần viết lại toàn bộ đối tượng. Nếu bạn muốn cho phép người dùng cập nhật hồ sơ của họ bạn có thể cập nhật tên người dùng như sau:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

// Only update the name, leave the age and address!
await ref.update({
  "age": 19,
});

Phương thức update() chấp nhận một đường dẫn phụ đến các nút, cho phép bạn cập nhật nhiều các nút trên cơ sở dữ liệu cùng một lúc:

DatabaseReference ref = FirebaseDatabase.instance.ref("users");

await ref.update({
  "123/age": 19,
  "123/address/line1": "1 Mountain View",
});

Đọc dữ liệu

Đọc dữ liệu bằng cách theo dõi các sự kiện giá trị

Để đọc dữ liệu tại một đường dẫn và theo dõi các thay đổi, hãy sử dụng Thuộc tính onValue của DatabaseReference cần theo dõi DatabaseEvent giây.

Bạn có thể sử dụng DatabaseEvent để đọc dữ liệu tại một đường dẫn nhất định, như tồn tại vào thời điểm diễn ra sự kiện. Sự kiện này được kích hoạt một lần khi trình nghe được đính kèm và lặp lại mỗi lần dữ liệu, bao gồm bất kỳ phần tử con nào, thay đổi. Sự kiện có một tài sản snapshot chứa tất cả dữ liệu ở đó thông tin vị trí, bao gồm cả dữ liệu của trẻ. Nếu không có dữ liệu, Thuộc tính exists sẽ là false và thuộc tính value của thuộc tính này sẽ là rỗng.

Ví dụ sau minh hoạ một ứng dụng viết blog trên mạng xã hội truy xuất thông tin chi tiết về một bài đăng trong cơ sở dữ liệu:

DatabaseReference starCountRef =
        FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
    final data = event.snapshot.value;
    updateStarCount(data);
});

Trình nghe sẽ nhận được một DataSnapshot chứa dữ liệu theo phương thức đã chỉ định vị trí trong cơ sở dữ liệu tại thời điểm diễn ra sự kiện trong thuộc tính value.

Đọc dữ liệu một lần

Đọc một lần bằng get()

SDK này được thiết kế để quản lý hoạt động tương tác với máy chủ cơ sở dữ liệu cho dù đang trực tuyến hoặc ngoại tuyến.

Thông thường, bạn nên sử dụng các kỹ thuật sự kiện giá trị được mô tả ở trên để đọc để nhận thông báo về nội dung cập nhật đối với dữ liệu từ phần phụ trợ. Những kỹ thuật đó giảm mức sử dụng và thanh toán, đồng thời được tối ưu hoá để cung cấp cho người dùng trải nghiệm trực tuyến và ngoại tuyến.

Nếu chỉ cần dữ liệu một lần, bạn có thể sử dụng get() để có ảnh chụp nhanh khỏi cơ sở dữ liệu. Nếu vì bất kỳ lý do gì, get() không thể trả lại máy chủ, máy khách sẽ thăm dò bộ nhớ đệm của bộ nhớ cục bộ và trả về một lỗi nếu vẫn không tìm thấy giá trị đó.

Ví dụ sau minh hoạ việc truy xuất tên người dùng công khai của một người dùng một lần duy nhất từ cơ sở dữ liệu:

final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
    print(snapshot.value);
} else {
    print('No data available.');
}

Việc sử dụng get() không cần thiết có thể làm tăng mức sử dụng băng thông và dẫn đến mất mát về hiệu suất. Bạn có thể ngăn chặn điều này bằng cách sử dụng trình nghe theo thời gian thực như minh hoạ ở trên.

Đọc dữ liệu một lần bằng once()

Trong một số trường hợp, bạn có thể muốn trả về giá trị từ bộ nhớ đệm cục bộ ngay lập tức, thay vì kiểm tra giá trị đã cập nhật trên máy chủ. Trong các Bạn có thể dùng once() để lấy dữ liệu từ bộ nhớ đệm của ổ đĩa cục bộ ngay lập tức.

Điều này hữu ích đối với dữ liệu chỉ cần tải một lần và dự kiến sẽ không thay đổi thường xuyên hoặc cần chủ động lắng nghe. Ví dụ: ứng dụng viết blog trong các ví dụ trước sẽ sử dụng phương thức này để tải hồ sơ của người dùng khi họ bắt đầu viết một bài đăng mới:

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

Cập nhật hoặc xoá dữ liệu

Cập nhật các trường cụ thể

Để ghi đồng thời vào các phần tử con cụ thể của một nút mà không ghi đè lên các nút khác nút con, hãy sử dụng phương thức update().

Khi gọi update(), bạn có thể cập nhật các giá trị con ở cấp thấp hơn bằng cách chỉ định đường dẫn cho khoá. Nếu dữ liệu được lưu trữ ở nhiều vị trí để mở rộng quy mô tốt hơn, bạn có thể cập nhật tất cả các phiên bản của dữ liệu đó bằng cách sử dụng ngừng sử dụng dữ liệu. Ví dụ: một ứng dụng viết blog xã hội có thể muốn tạo một bài đăng và đồng thời cập nhật bài đăng đó nguồn cấp dữ liệu hoạt động gần đây và nguồn cấp dữ liệu hoạt động của người dùng đăng bài. Để làm được điều này, ứng dụng viết blog sử dụng mã như sau:

void writeNewPost(String uid, String username, String picture, String title,
        String body) async {
    // A post entry.
    final postData = {
        'author': username,
        'uid': uid,
        'body': body,
        'title': title,
        'starCount': 0,
        'authorPic': picture,
    };

    // Get a key for a new Post.
    final newPostKey =
        FirebaseDatabase.instance.ref().child('posts').push().key;

    // Write the new post's data simultaneously in the posts list and the
    // user's post list.
    final Map<String, Map> updates = {};
    updates['/posts/$newPostKey'] = postData;
    updates['/user-posts/$uid/$newPostKey'] = postData;

    return FirebaseDatabase.instance.ref().update(updates);
}

Ví dụ này sử dụng push() để tạo bài đăng trong nút chứa bài đăng cho tất cả người dùng tại /posts/$postid và truy xuất khoá đồng thời bằng key. Sau đó, khoá này có thể được dùng để tạo mục nhập thứ hai trong tài khoản bài đăng tại /user-posts/$userid/$postid.

Khi sử dụng các đường dẫn này, bạn có thể cập nhật đồng thời nhiều vị trí trong cây JSON với một lệnh gọi đến update(), chẳng hạn như trong ví dụ này tạo bài đăng mới ở cả hai vị trí. Cập nhật đồng thời theo cách này rất quan trọng: tất cả cập nhật thành công hoặc tất cả cập nhật đều không thành công.

Thêm lệnh gọi lại hoàn thành

Nếu muốn biết thời điểm dữ liệu của bạn đã được cam kết, bạn có thể đăng ký lệnh gọi lại hoàn thành. Cả set()update() đều trả về Future. bạn có thể đính kèm các lệnh gọi lại thành công và lỗi được gọi khi quá trình ghi cam kết với cơ sở dữ liệu và khi cuộc gọi không thành công.

FirebaseDatabase.instance
    .ref('users/$userId/email')
    .set(emailAddress)
    .then((_) {
        // Data saved successfully!
    })
    .catchError((error) {
        // The write failed...
    });

Xóa dữ liệu

Cách đơn giản nhất để xoá dữ liệu là gọi remove() trên một tham chiếu đến vị trí của dữ liệu đó.

Bạn cũng có thể xoá bằng cách chỉ định giá trị rỗng cho một thao tác ghi khác chẳng hạn như set() hoặc update(). Bạn có thể sử dụng kỹ thuật này với update() để xoá nhiều phần tử con trong một lệnh gọi API.

Lưu dữ liệu dưới dạng giao dịch

Khi làm việc với dữ liệu có thể bị hỏng do các nội dung sửa đổi đồng thời, chẳng hạn như bộ đếm gia tăng, bạn có thể sử dụng giao dịch bằng cách truyền trình xử lý giao dịch cho runTransaction(). Trình xử lý giao dịch sẽ lấy trạng thái hiện tại của dữ liệu làm đối số và sẽ trả về trạng thái mong muốn mới mà bạn muốn viết. Nếu một khách hàng khác ghi vào vị trí trước khi giá trị mới được ghi thành công, hàm cập nhật được gọi lại với giá trị hiện tại mới và giá trị ghi là đã thử lại.

Ví dụ: trong ứng dụng viết blog xã hội mẫu, bạn có thể cho phép người dùng gắn dấu sao và bỏ gắn dấu sao bài đăng, đồng thời theo dõi số sao mà một bài đăng đã nhận được như sau:

void toggleStar(String uid) async {
  DatabaseReference postRef =
      FirebaseDatabase.instance.ref("posts/foo-bar-123");

  TransactionResult result = await postRef.runTransaction((Object? post) {
    // Ensure a post at the ref exists.
    if (post == null) {
      return Transaction.abort();
    }

    Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
    if (_post["stars"] is Map && _post["stars"][uid] != null) {
      _post["starCount"] = (_post["starCount"] ?? 1) - 1;
      _post["stars"][uid] = null;
    } else {
      _post["starCount"] = (_post["starCount"] ?? 0) + 1;
      if (!_post.containsKey("stars")) {
        _post["stars"] = {};
      }
      _post["stars"][uid] = true;
    }

    // Return the new data.
    return Transaction.success(_post);
  });
}

Theo mặc định, các sự kiện sẽ được gửi mỗi khi chạy hàm cập nhật giao dịch, để chạy hàm này nhiều lần, bạn có thể thấy các trạng thái trung gian. Bạn có thể đặt applyLocally thành false để loại bỏ các trạng thái trung gian này và thay vào đó hãy đợi cho đến khi giao dịch hoàn tất trước khi các sự kiện được đưa ra:

await ref.runTransaction((Object? post) {
  // ...
}, applyLocally: false);

Kết quả của một giao dịch là TransactionResult, trong đó có chứa thông tin chẳng hạn như liệu giao dịch đã được cam kết hay chưa và thông tin tổng quan nhanh mới:

DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");

TransactionResult result = await ref.runTransaction((Object? post) {
  // ...
});

print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot

Hủy giao dịch

Nếu bạn muốn huỷ một giao dịch một cách an toàn, hãy gọi Transaction.abort() để gửi một AbortTransactionException:

TransactionResult result = await ref.runTransaction((Object? user) {
  if (user !== null) {
    return Transaction.abort();
  }

  // ...
});

print(result.committed); // false

Số gia ở phía máy chủ nguyên tử

Trong trường hợp sử dụng trên, chúng ta viết hai giá trị vào cơ sở dữ liệu: mã nhận dạng của người dùng gắn dấu sao/bỏ gắn dấu sao bài đăng và số sao tăng lên. Nếu chúng tôi đã biết rằng người dùng đang gắn dấu sao bài đăng, chúng ta có thể sử dụng gia số nguyên tử thay vì một giao dịch.

void addStar(uid, key) async {
  Map<String, Object?> updates = {};
  updates["posts/$key/stars/$uid"] = true;
  updates["posts/$key/starCount"] = ServerValue.increment(1);
  updates["user-posts/$key/stars/$uid"] = true;
  updates["user-posts/$key/starCount"] = ServerValue.increment(1);
  return FirebaseDatabase.instance.ref().update(updates);
}

Mã này không sử dụng hoạt động giao dịch nên không tự động nhận được chạy lại nếu có bản cập nhật xung đột. Tuy nhiên, vì phép toán tăng xảy ra trực tiếp trên máy chủ cơ sở dữ liệu, nên không có khả năng xảy ra xung đột.

Nếu bạn muốn phát hiện và từ chối xung đột dành riêng cho ứng dụng, chẳng hạn như một người dùng gắn dấu sao bài đăng mà họ đã gắn dấu sao trước đó, bạn nên viết quy tắc bảo mật cho trường hợp sử dụng đó.

Làm việc với dữ liệu khi không có mạng

Nếu máy khách bị mất kết nối mạng, ứng dụng của bạn sẽ tiếp tục hoạt động chính xác.

Mỗi ứng dụng được kết nối với cơ sở dữ liệu Firebase đều duy trì phiên bản nội bộ riêng của mọi dữ liệu đang hoạt động. Khi được ghi, dữ liệu sẽ được ghi vào phiên bản cục bộ này đầu tiên. Sau đó, ứng dụng Firebase sẽ đồng bộ hoá dữ liệu đó với cơ sở dữ liệu từ xa máy chủ của bạn và với các ứng dụng khách khác với "nỗ lực tối đa" cơ sở.

Do đó, tất cả hoạt động ghi vào cơ sở dữ liệu sẽ kích hoạt các sự kiện cục bộ ngay lập tức, trước khi mọi dữ liệu đều được ghi vào máy chủ. Tức là ứng dụng của bạn vẫn hoạt động thích ứng bất kể độ trễ hoặc khả năng kết nối mạng như thế nào.

Sau khi kết nối được thiết lập lại, ứng dụng của bạn sẽ nhận được nhóm các sự kiện để máy khách đồng bộ hóa với trạng thái máy chủ hiện tại mà không phải viết bất kỳ mã tuỳ chỉnh nào.

Chúng ta sẽ nói thêm về hành vi ngoại tuyến trong Tìm hiểu thêm về các chức năng trực tuyến và ngoại tuyến.

Các bước tiếp theo