Đọc và ghi dữ liệu

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

Trước khi nói về cách ứng dụng của bạn đọc và ghi vào Cơ sở dữ liệu theo thời gian thực, hãy giới thiệu một bộ công cụ mà bạn có thể dùng để tạo mẫu và kiểm thử chức năng Cơ sở dữ liệu theo thời gian thực: Bộ công cụ mô phỏng Firebase. Nếu bạn đang thử các mô hình dữ liệu khác nhau, tối ưu hoá các quy tắc bảo mật hoặc tìm cách tương tác với phần phụ trợ hiệu quả nhất về chi phí, thì việc có thể làm việc cục bộ mà không cần triển khai các dịch vụ trực tiếp có thể là một ý 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ộ công cụ mô phỏng. Bộ công cụ này cho phép ứng dụng của bạn tương tác với nội dung và cấu hình cơ sở dữ liệu được mô phỏng, cũng như các tài nguyên dự án được mô phỏng (các hàm, cơ sở dữ liệu khác và quy tắc bảo mật) (nếu có).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ỉ cần 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. Từ gốc của thư mục dự án cục bộ, hãy chạy firebase emulators:start.
  3. Thực hiện các lệnh gọi từ mã nguyên mẫu của ứng dụng bằng cách sử dụng SDK nền tảng Cơ sở dữ liệu theo thời gian thực như bình thường hoặc sử dụng API REST Cơ sở dữ liệu theo thời gian thực.

Bạn có thể xem hướng dẫn chi tiết liên quan đến Cơ sở dữ liệu theo thời gian thực và Cloud Functions. Bạn cũng nên xem giới thiệu về Bộ công cụ mô phỏng.

Lấy DatabaseReference

Để đọc hoặc ghi dữ liệu vào cơ sở dữ liệu, bạn cần có một phiên bản của DatabaseReference:

DatabaseReference ref = FirebaseDatabase.instance.ref();

Ghi dữ liệu

Tài liệu này trình bày những 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 một DatabaseReference và được truy xuất bằng cách chờ hoặc theo dõi các sự kiện do tham chiếu phát ra. Các sự kiện được phát một lần cho trạng thái ban đầu của dữ liệu và phát lại bất cứ khi nào dữ liệu thay đổi.

Các 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 tham chiếu đã chỉ định, thay thế mọi dữ liệu hiện có tại đường dẫn đó. Bạn có thể đặt một giá trị tham chiếu cho các loại sau: String, boolean, int, double, Map, List.

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

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

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

Khi sử dụng set() theo cách này, bạn sẽ ghi đè dữ liệu tại vị trí đã chỉ định, bao gồm cả mọi nút con. Tuy nhiên, bạn vẫn có thể cập nhật một thành phần con mà không cần viết lại toàn bộ đối tượng. Nếu 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 age, leave the name 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 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 để theo dõi DatabaseEvent.

Bạn có thể sử dụng DatabaseEvent để đọc dữ liệu tại một đường dẫn nhất định, như dữ liệu tồn tại tại thời điểm xảy 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à một lần nữa mỗi khi dữ liệu (bao gồm cả mọi thành phần con) thay đổi. Sự kiện này có một thuộc tính snapshot chứa tất cả dữ liệu tại vị trí đó, bao gồm cả dữ liệu con. Nếu không có dữ liệu, thuộc tính exists của ảnh chụp nhanh sẽ là false và thuộc tính value sẽ là giá trị rỗng.

Ví dụ sau đây minh hoạ một ứng dụng blog xã hội truy xuất thông tin chi tiết của một bài đăng từ 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 nhận được một DataSnapshot chứa dữ liệu tại vị trí được chỉ định trong cơ sở dữ liệu tại thời điểm xảy 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 được thiết kế để quản lý các hoạt động tương tác với máy chủ cơ sở dữ liệu, cho dù ứng dụng của bạn đang trực tuyến hay 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 dữ liệu nhằm nhận được thông báo về các bản cập nhật dữ liệu từ phần phụ trợ. Những kỹ thuật này giúp giảm mức sử dụng và phí thanh toán, đồng thời được tối ưu hoá để mang đến cho người dùng trải nghiệm tốt nhất khi họ truy cập mạng và không truy cập mạng.

Nếu chỉ cần dữ liệu một lần, bạn có thể dùng get() để lấy thông tin tổng quan nhanh về dữ liệu từ cơ sở dữ liệu. Nếu vì lý do nào đó mà get() không thể trả về giá trị máy chủ, thì máy khách sẽ kiểm tra bộ nhớ đệm của bộ nhớ cục bộ và trả về lỗi nếu vẫn không tìm thấy giá trị.

Ví dụ sau đây minh hoạ cách truy xuất tên người dùng công khai của người dùng một lần 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 giảm hiệu suất. Bạn có thể ngăn chặn điều này bằng cách sử dụng một 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 giá trị từ bộ nhớ đệm cục bộ được trả về ngay lập tức, thay vì kiểm tra giá trị đã cập nhật trên máy chủ. Trong những trường hợp đó, bạn có thể sử dụng once() để lấy dữ liệu ngay lập tức từ bộ nhớ đệm của ổ đĩa cục bộ.

Điều này hữu ích cho dữ liệu chỉ cần tải một lần và không dự kiến thay đổi thường xuyên hoặc yêu cầu lắng nghe chủ động. Ví dụ: ứng dụng viết blog trong các ví dụ trước sử dụng phương thức này để tải hồ sơ 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 đè các nút con khác, 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 một đườ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 phân đầu ra dữ liệu. Ví dụ: một ứng dụng 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 đó vào 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 việc này, ứng dụng viết blog sẽ 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 một bài đăng trong nút chứa các bài đăng cho tất cả người dùng tại /posts/$postid và đồng thời truy xuất khoá bằng key. Sau đó, khoá này có thể được dùng để tạo mục thứ hai trong bài đăng của người dùng tại /user-posts/$userid/$postid.

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

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

Nếu muốn biết thời điểm dữ liệu của bạn được xác nhận, bạn có thể đăng ký các lệnh gọi lại hoàn tất. 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 thao tác ghi đã được xác nhận với cơ sở dữ liệu và khi lệnh 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 thành phần con trong một lệnh gọi API duy nhất.

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 hoạt động sửa đổi đồng thời, chẳng hạn như bộ đếm gia tăng, bạn có thể sử dụng một giao dịch bằng cách truyền trình xử lý giao dịch đến runTransaction(). Trình xử lý giao dịch lấy trạng thái hiện tại của dữ liệu làm đối số và trả về trạng thái mới mà bạn muốn ghi. Nếu một ứng dụng khác ghi vào vị trí này trước khi giá trị mới của bạn được ghi thành công, thì hàm cập nhật sẽ được gọi lại bằng giá trị hiện tại mới và thao tác ghi sẽ được thử lại.

Ví dụ: trong ứng dụng 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 cho bài đăng, đồng thời theo dõi số lượng dấu 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 tạo mỗi khi hàm cập nhật giao dịch chạy. Vì vậy, nếu chạy hàm 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 để ngăn chặn các trạng thái trung gian này và thay vào đó, đợi cho đến khi giao dịch hoàn tất trước khi các sự kiện được kích hoạt:

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

Kết quả của một giao dịch là TransactionResult, chứa thông tin như giao dịch đã được thực hiện hay chưa và ảnh chụp 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() để truyền một AbortTransactionException:

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

  // ...
});

print(result.committed); // false

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

Trong trường hợp sử dụng trên, chúng ta đang ghi 2 giá trị vào cơ sở dữ liệu: mã nhận dạng của người dùng gắn/huỷ gắn dấu sao bài đăng và số lượng dấu sao được tăng lên. Nếu đã biết người dùng đang gắn dấu sao cho bài đăng, chúng ta có thể sử dụng thao tác tăng dần đơn vị thay vì 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 thao tác giao dịch, nên không tự động chạy lại nếu có một bản cập nhật xung đột. Tuy nhiên, vì thao tác 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 muốn phát hiện và từ chối các xung đột dành riêng cho ứng dụng, chẳng hạn như người dùng gắn dấu sao vào một bài đăng mà họ đã gắn dấu sao trước đó, bạn nên viết các quy tắc bảo mật tuỳ chỉnh 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ột máy khách mất kết nối mạng, ứng dụng của bạn sẽ tiếp tục hoạt động bình thường.

Mọi ứng dụng 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 dữ liệu được ghi, dữ liệu sẽ được ghi vào phiên bản cục bộ này trước. Sau đó, ứng dụng Firebase sẽ đồng bộ hoá dữ liệu đó với các máy chủ cơ sở dữ liệu từ xa và với các ứng dụng khác theo cơ chế "nỗ lực tối đa".

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

Sau khi kết nối được thiết lập lại, ứng dụng của bạn sẽ nhận được bộ sự kiện thích hợp để ứng dụng đồng bộ hoá với trạng thái hiện tại của máy chủ mà không cần 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 phần Tìm hiểu thêm về các tính năng trực tuyến và ngoại tuyến.

Các bước tiếp theo