Đọc và ghi dữ liệu

(Tùy chọn) Nguyên mẫu và thử nghiệm với Bộ 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 thời gian thực, hãy giới thiệu một bộ công cụ bạn có thể sử dụng để tạo nguyên mẫu và kiểm tra chức năng Cơ sở dữ liệu thời gian thực: Bộ mô phỏng Firebase. Nếu bạn đang thử các mô hình dữ liệu khác nhau, tối ưu hóa các quy tắc bảo mật hoặc nỗ lực tìm ra cách hiệu quả nhất về mặt chi phí để tương tác với back-end, 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 tuyệt vời.

Trình mô phỏng cơ sở dữ liệu thời gian thực là một phần của Bộ mô phỏng, 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 (chức năng, cơ sở dữ liệu khác và quy tắc bảo mật) theo tùy chọn của bạn).emulator_suite_short

Sử dụng trình mô phỏng Cơ sở dữ liệu 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 thử nghiệm của ứng dụng để kết nối với trình mô phỏng.
  2. Từ thư mục gốc của thư mục dự án cục bộ của bạn, hãy chạy firebase emulators:start .
  3. Thực hiện 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 thời gian thực như bình thường hoặc sử dụng API REST của cơ sở dữ liệu thời gian thực.

Hiện có sẵn hướng dẫn chi tiết liên quan đến Cơ sở dữ liệu thời gian thực và Chức năng đám mây . Bạn cũng nên xem phần giới thiệu về Emulator Suite .

Nhận một tài liệu tham khảo cơ sở dữ liệu

Để đọc hoặc ghi dữ liệu từ cơ sở dữ liệu, bạn cần 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 và ghi dữ liệu Firebase.

Dữ liệu Firebase được ghi vào DatabaseReference và được truy xuất bằng cách chờ hoặc lắng nghe các sự kiện do tham chiếu phát 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à một lần nữa 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 tham chiếu cho các loại sau: String , boolean , int , double , Map , List .

Chẳng hạn, 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"
  }
});

Sử dụng set() theo cách này sẽ ghi đè dữ liệu tại vị trí đã chỉ định, bao gồm mọi nút con. Tuy nhiên, bạn vẫn có thể cập nhật một đối tượng con 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 đường dẫn phụ tới 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 lắng nghe các sự kiện giá trị

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

Bạn có thể sử dụng DatabaseEvent để đọc dữ liệu tại một đường dẫn nhất định vì nó tồn tại tại 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 khi dữ liệu, bao gồm cả dữ liệu con, thay đổi. Sự kiện này có 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ẽ false và thuộc tính value của nó sẽ không có giá trị.

Ví dụ sau minh họa một ứng dụng viết 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ủa nó.

Đọ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 tương tác với máy chủ cơ sở dữ liệu cho dù ứng dụng của bạn trực tuyến hay ngoại tuyến.

Nói chung, 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 thông báo về các cập nhật dữ liệu từ chương trình phụ trợ. Những kỹ thuật đó giúp giảm mức sử dụng và thanh toán của bạn, đồng thời được tối ưu hóa để mang lại cho người dùng trải nghiệm tốt nhất khi họ trực tuyến và ngoại tuyến.

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

Ví dụ sau minh họa việc 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, điều này có thể được ngăn chặn bằng cách sử dụng trình nghe thời gian thực như minh họa ở trên.

Đọc dữ liệu một lần với một lần()

Trong một số trường hợp, bạn có thể muốn giá trị từ bộ đệ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 từ bộ đệm đĩ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 yêu cầu lắng nghe tích cực. Ví dụ: ứng dụng viết blog trong các ví dụ trước sử dụng phương pháp 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 xóa dữ liệu

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

Để ghi đồng thời vào các nút con cụ thể của một nút mà không ghi đè lên 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 đường dẫn cho khóa. 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 xuất 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 đó lên 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 đăng bài. Để làm điều này, ứng dụng viết blog sử dụng mã như thế này:

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 khóa bằng key . Sau đó, khóa này có thể được sử dụng để tạo mục nhập 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ập nhật đồng thời cho nhiều vị trí trong cây JSON chỉ bằng một lệnh gọi tới 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 mang tính nguyên tử: 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 thành

Nếu bạn muốn biết khi nào dữ liệu của mình đượ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 s, mà 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 lệnh ghi đã được cam kết vào 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 để xóa dữ liệu là gọi hàm remove() trên tham chiếu đến vị trí của dữ liệu đó.

Bạn cũng có thể xóa bằng cách chỉ định null làm giá trị cho 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() để xóa 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 sửa đổi đồng thời, chẳng hạn như bộ đếm tăng dần, bạn có thể sử dụng giao dịch bằng cách chuyển trình xử lý giao dịch tới 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 mong muốn mới mà bạn muốn ghi. Nếu một ứng dụng khách khác ghi vào vị trí trước khi giá trị mới của bạn được ghi thành công, hàm cập nhật của bạn sẽ được gọi lại với giá trị hiện tại mới và quá trình ghi sẽ được 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 cho bài đăng cũng như theo dõi số lượng 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 được đưa ra mỗi khi chức năng cập nhật giao dịch chạy, do đó bạn chạy chức năng 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 giao dịch là TransactionResult , chứa thông tin như liệu giao dịch có được thực hiện hay không 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 hủy giao dịch một cách an toàn, hãy gọi Transaction.abort() để ném AbortTransactionException :

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

  // ...
});

print(result.committed); // false

Gia tăng phía máy chủ nguyên tử

Trong trường hợp sử dụng ở trên, chúng tôi đang ghi hai giá trị vào cơ sở dữ liệu: ID của người dùng đã gắn dấu sao/bỏ gắn dấu sao cho bài đăng và số lượng sao tăng dần. Nếu chúng tôi đã biết người dùng đó đang gắn dấu sao cho bài đăng, chúng tôi có thể sử dụng thao tác tăng dần 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 nó không tự động chạy lại nếu có bản cập nhật xung đột. Tuy nhiên, vì thao tác gia tăng diễn 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 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 cho 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 tùy chỉnh cho trường hợp sử dụng đó.

Làm việc với dữ liệu ngoại tuyến

Nếu khách hàng 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 khách hàng được kết nối với cơ sở dữ liệu Firebase sẽ duy trì phiên bản nội bộ của bất kỳ dữ liệu đang hoạt động nào. Khi dữ liệu được ghi, trước tiên nó sẽ được ghi vào phiên bản cục bộ này. Sau đó, ứng dụng khách Firebase sẽ đồng bộ hóa 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ách khác trên cơ sở "nỗ lực tối đa".

Kết quả là, tất cả các thao tác 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 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ễ mạng hoặc khả năng kết nối.

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 sự kiện thích hợp để ứng dụng khách đồng bộ hóa với trạng thái máy chủ hiện tại mà không cần phải viết bất kỳ mã tùy chỉnh nào.

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

Bước tiếp theo