Đọc và ghi dữ liệu trên web

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

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

Trình mô phỏng Realtime Database là một phần của Local Emulator Suite, cho phép ứng dụng 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 (hàm, cơ sở dữ liệu khác và quy tắc bảo mật) nếu muốn.

Bạn chỉ cần thực hiện vài bước để sử dụng trình mô phỏng Realtime Database:

  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. Chạy firebase emulators:start từ thư mục gốc của dự án cục bộ.
  3. Thực hiện lệnh gọi từ mã nguyên mẫu của ứng dụng bằng SDK nền tảng Realtime Database như bình thường hoặc sử dụng API REST Realtime Database.

Bạn có thể xem hướng dẫn chi tiết liên quan đến Realtime DatabaseCloud Functions. Bạn cũng nên xem giới thiệu về Local Emulator Suite.

Lấy tham chiếu cơ sở dữ liệu

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

Web

import { getDatabase } from "firebase/database";

const database = getDatabase();

Web

var database = firebase.database();

Ghi dữ liệu

Tài liệu này trình bày các thông tin cơ bản về cách truy xuất dữ liệu cũng như cách sắp xếp và lọc dữ liệu Firebase.

Dữ liệu Firebase được truy xuất bằng cách đính kèm trình nghe không đồng bộ vào firebase.database.Reference. Trình nghe được kích hoạt 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 được chỉ định, thay thế mọi dữ liệu hiện có tại đường dẫn đó. Ví dụ: một ứng dụng viết blog trên mạng xã hội có thể thêm người dùng bằng set() như sau:

Web

import { getDatabase, ref, set } from "firebase/database";

function writeUserData(userId, name, email, imageUrl) {
  const db = getDatabase();
  set(ref(db, 'users/' + userId), {
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

Web

function writeUserData(userId, name, email, imageUrl) {
  firebase.database().ref('users/' + userId).set({
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

Việc sử dụng set() sẽ ghi đè dữ liệu tại vị trí đã chỉ định, bao gồm cả mọi nút con.

Đọc dữ liệu

Theo dõi 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 onValue() để quan sát các sự kiện. Bạn có thể sử dụng sự kiện này để đọc ảnh chụp nhanh tĩnh của nội dung tại một đường dẫn nhất định, vì nội dung đó tồn tại tại thời điểm xảy ra sự kiện. Phương thức 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ả dữ liệu con, thay đổi. Lệnh gọi lại sự kiện được truyền một ảnh chụp nhanh 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, ảnh chụp nhanh sẽ trả về false khi bạn gọi exists()null khi bạn gọi val() trên đó.

Ví dụ sau đây minh hoạ một ứng dụng blog xã hội truy xuất số lượng sao của một bài đăng từ cơ sở dữ liệu:

Web

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const starCountRef = ref(db, 'posts/' + postId + '/starCount');
onValue(starCountRef, (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

Web

var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

Trình nghe sẽ nhận được snapshot 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. Bạn có thể truy xuất dữ liệu trong snapshot bằng phương thức val().

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

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

SDK này đượ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 ở chế độ trực tuyến hay ngoại tuyến.

Nhìn 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ề nội dung cập nhật dữ liệu từ phần phụ trợ. Các kỹ thuật trình nghe giúp giảm mức sử dụng và mức thanh toán, đồng thời được tối ưu hoá để mang lại cho người dùng trải nghiệm tốt nhất khi họ truy cập mạng và khi không có mạng.

Nếu chỉ cần dữ liệu một lần, bạn có thể sử 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ì ứng dụng sẽ thăm dò 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ị.

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 trình nghe theo thời gian thực như minh hoạ ở trên.

Web

import { getDatabase, ref, child, get } from "firebase/database";

const dbRef = ref(getDatabase());
get(child(dbRef, `users/${userId}`)).then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

Web

const dbRef = firebase.database().ref();
dbRef.child("users").child(userId).get().then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

Đọc dữ liệu một lần bằng trình quan sát

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 từ bộ nhớ đệm ổ đĩa cục bộ ngay lập tức.

Điều này hữu ích cho dữ liệu chỉ cần tải một lần và không được dự kiến sẽ thay đổi thường xuyên hoặc yêu cầu phải 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ơ của người dùng khi họ bắt đầu viết bài đăng mới:

Web

import { getDatabase, ref, onValue } from "firebase/database";
import { getAuth } from "firebase/auth";

const db = getDatabase();
const auth = getAuth();

const userId = auth.currentUser.uid;
return onValue(ref(db, '/users/' + userId), (snapshot) => {
  const username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
}, {
  onlyOnce: true
});

Web

var userId = firebase.auth().currentUser.uid;
return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => {
  var username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
});

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

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

Để đồng thời ghi vào các 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 thực thể của dữ liệu đó bằng cách sử dụng tính năng phân phối dữ liệu.

Ví dụ: một ứng dụng blog xã hội có thể 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ằng cách sử dụng mã như sau:

Web

import { getDatabase, ref, child, push, update } from "firebase/database";

function writeNewPost(uid, username, picture, title, body) {
  const db = getDatabase();

  // A post entry.
  const postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  const newPostKey = push(child(ref(db), 'posts')).key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  const updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return update(ref(db), updates);
}

Web

function writeNewPost(uid, username, picture, title, body) {
  // A post entry.
  var postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  var newPostKey = firebase.database().ref().child('posts').push().key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  var updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return firebase.database().ref().update(updates);
}

Ví dụ này sử dụng push() để tạo một bài đăng trong nút chứa bài đăng cho tất cả người dùng tại /posts/$postid và đồng thời truy xuất khoá. Sau đó, khoá này có thể được 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.

Khi sử dụng các đường dẫn này, bạn có thể thực hiện đồng thời việc cập nhật 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 là 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 khi hoàn tất

Nếu muốn biết thời điểm dữ liệu được xác nhận, bạn có thể thêm một lệnh gọi lại hoàn tất. Cả set()update() đều thực hiện lệnh gọi lại hoàn tất không bắt buộc được gọi khi quá trình ghi đã được xác nhận vào cơ sở dữ liệu. Nếu lệnh gọi không thành công, lệnh gọi lại sẽ được truyền một đối tượng lỗi cho biết lý do xảy ra lỗi.

Web

import { getDatabase, ref, set } from "firebase/database";

const db = getDatabase();
set(ref(db, 'users/' + userId), {
  username: name,
  email: email,
  profile_picture : imageUrl
})
.then(() => {
  // Data saved successfully!
})
.catch((error) => {
  // The write failed...
});

Web

firebase.database().ref('users/' + userId).set({
  username: name,
  email: email,
  profile_picture : imageUrl
}, (error) => {
  if (error) {
    // The write failed...
  } else {
    // Data saved successfully!
  }
});

Xóa dữ liệu

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

Bạn cũng có thể xoá bằng cách chỉ định null làm giá trị 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.

Nhận Promise

Để biết thời điểm dữ liệu của bạn được cam kết với máy chủ Firebase Realtime Database, bạn có thể sử dụng Promise. Cả set()update() đều có thể trả về một Promise mà bạn có thể sử dụng để biết thời điểm ghi được thực hiện đối với cơ sở dữ liệu.

Tách trình nghe

Bạn có thể xoá lệnh gọi lại bằng cách gọi phương thức off() trên tham chiếu cơ sở dữ liệu Firebase.

Bạn có thể xoá một trình nghe bằng cách truyền trình nghe đó dưới dạng tham số đến off(). Việc gọi off() trên vị trí không có đối số sẽ xoá tất cả trình nghe tại vị trí đó.

Việc gọi off() trên trình nghe mẹ không tự động xoá trình nghe đã đăng ký trên các nút con; bạn cũng phải gọi off() trên mọi trình nghe con để xoá lệnh gọi lại.

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

Khi xử lý dữ liệu có thể bị hỏng do các thay đổi đồng thời, chẳng hạn như bộ đếm gia tăng, bạn có thể sử dụng thao tác giao dịch. Bạn có thể cung cấp cho thao tác này một hàm cập nhật và một hàm gọi lại hoàn tất không bắt buộc. Hàm cập nhật 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í trước khi giá trị mới của bạn được ghi thành công, thì hàm cập nhật của bạn 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 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:

Web

import { getDatabase, ref, runTransaction } from "firebase/database";

function toggleStar(uid) {
  const db = getDatabase();
  const postRef = ref(db, '/posts/foo-bar-123');

  runTransaction(postRef, (post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

Web

function toggleStar(postRef, uid) {
  postRef.transaction((post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

Việc sử dụng giao dịch sẽ giúp số lượng dấu sao không bị sai nếu nhiều người dùng gắn dấu sao cho cùng một bài đăng cùng một lúc hoặc ứng dụng có dữ liệu cũ. Nếu giao dịch bị từ chối, máy chủ sẽ trả về giá trị hiện tại cho ứng dụng. Ứng dụng này sẽ chạy lại giao dịch bằng giá trị đã cập nhật. Quá trình này lặp lại cho đến khi giao dịch được chấp nhận hoặc bạn huỷ giao dịch.

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

Trong trường hợp sử dụng ở trên, chúng ta sẽ ghi 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 cho bài đăng và số lượng dấu sao đã 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 nguyên tử thay vì giao dịch.

Web

function addStar(uid, key) {
  import { getDatabase, increment, ref, update } from "firebase/database";
  const dbRef = ref(getDatabase());

  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = increment(1);
  update(dbRef, updates);
}

Web

function addStar(uid, key) {
  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  firebase.database().ref().update(updates);
}

Mã này không sử dụng thao tác giao dịch, vì vậy, mã này sẽ 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 tăng giá trị 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 đánh dấu một bài đăng mà họ đã đánh dấu 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 ứng dụng 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 đúng cách.

Mỗi ứng dụng khách đượ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 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 trên cơ sở "tối đa".

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

Chúng ta sẽ thảo luận 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