อ่านและเขียนข้อมูลบนเว็บ

(ไม่บังคับ) สร้างต้นแบบและทดสอบด้วย Firebase Local Emulator Suite

ก่อนจะพูดถึงวิธีที่แอปอ่านและเขียนลงใน Realtime Database เราขอแนะนำชุดเครื่องมือที่คุณสามารถใช้สร้างต้นแบบและทดสอบฟังก์ชันการทำงานของ Realtime Database ดังนี้ Firebase Local Emulator Suite หากคุณกำลังลองใช้รูปแบบข้อมูลต่างๆ เพิ่มประสิทธิภาพกฎความปลอดภัย หรือพยายามหาวิธีที่คุ้มค่าที่สุดในการโต้ตอบกับแบ็กเอนด์ ความสามารถในการทํางานในเครื่องโดยไม่ต้องทําให้บริการใช้งานได้จริงอาจเป็นแนวคิดที่ยอดเยี่ยม

โปรแกรมจำลอง Realtime Database เป็นส่วนหนึ่งของ Local Emulator Suite ซึ่งช่วยให้แอปโต้ตอบกับเนื้อหาและการกำหนดค่าฐานข้อมูลที่จำลอง รวมถึงทรัพยากรโปรเจ็กต์ที่จำลอง (ฟังก์ชัน ฐานข้อมูลอื่นๆ และกฎการรักษาความปลอดภัย) ได้ด้วยหากต้องการ

การใช้โปรแกรมจำลอง Realtime Database มีเพียงไม่กี่ขั้นตอนดังนี้

  1. การเพิ่มโค้ด 1 บรรทัดลงในการกําหนดค่าการทดสอบของแอปเพื่อเชื่อมต่อกับโปรแกรมจําลอง
  2. จากรูทของไดเรกทอรีโปรเจ็กต์ในเครื่องโดยเรียกใช้ firebase emulators:start
  3. การเรียกใช้จากโค้ดโปรโตไทป์ของแอปโดยใช้ Realtime Database Platform SDK ตามปกติ หรือใช้ Realtime Database REST API

โปรดดูคำแนะนำโดยละเอียดเกี่ยวกับ Realtime Database และ Cloud Functions นอกจากนี้ คุณควรดูLocal Emulator Suite ข้อมูลเบื้องต้นด้วย

รับการอ้างอิงฐานข้อมูล

หากต้องการอ่านหรือเขียนข้อมูลจากฐานข้อมูล คุณต้องมีอินสแตนซ์ของ firebase.database.Reference ดังนี้

Web

import { getDatabase } from "firebase/database";

const database = getDatabase();

Web

var database = firebase.database();

เขียนข้อมูล

เอกสารนี้ครอบคลุมข้อมูลเบื้องต้นเกี่ยวกับการดึงข้อมูล รวมถึงวิธีจัดเรียงและกรองข้อมูล Firebase

ระบบจะดึงข้อมูล Firebase โดยการแนบตัวรับฟังแบบไม่พร้อมกันกับ firebase.database.Reference ระบบจะทริกเกอร์ Listener 1 ครั้งสำหรับสถานะเริ่มต้นของข้อมูล และทริกเกอร์อีกครั้งทุกครั้งที่ข้อมูลมีการเปลี่ยนแปลง

การดำเนินการเขียนพื้นฐาน

สําหรับการดําเนินการเขียนพื้นฐาน คุณสามารถใช้ set() เพื่อบันทึกข้อมูลไปยังการอ้างอิงที่ระบุ ซึ่งจะแทนที่ข้อมูลที่มีอยู่ซึ่งอยู่ในเส้นทางนั้น ตัวอย่างเช่น แอปพลิเคชันการเขียนบล็อกโซเชียลอาจเพิ่มผู้ใช้ด้วย set() ดังนี้

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
  });
}

การใช้ set() จะเขียนทับข้อมูลในตำแหน่งที่ระบุ รวมถึงโหนดย่อย

อ่านข้อมูล

ฟังเหตุการณ์ที่มีคุณค่า

หากต้องการอ่านข้อมูลในเส้นทางและรอการเปลี่ยนแปลง ให้ใช้ onValue() เพื่อสังเกตการณ์เหตุการณ์ คุณใช้เหตุการณ์นี้เพื่ออ่านสแนปชอตแบบคงที่ของเนื้อหาในเส้นทางที่ระบุได้ ตามที่มีอยู่ในขณะที่เกิดเหตุการณ์ เมธอดนี้จะทริกเกอร์ 1 ครั้งเมื่อมีการแนบ Listener และทริกเกอร์อีกครั้งทุกครั้งที่มีการเปลี่ยนแปลงข้อมูล รวมถึงรายการย่อย แคล็กแบ็กเหตุการณ์จะได้รับสแนปชอตที่มีข้อมูลทั้งหมดในตำแหน่งนั้น รวมถึงข้อมูลย่อย หากไม่มีข้อมูล สแนปชอตจะแสดง false เมื่อคุณเรียกใช้ exists() และ null เมื่อคุณเรียกใช้ val()

ตัวอย่างต่อไปนี้แสดงแอปพลิเคชันการเขียนบล็อกโซเชียลที่เรียกจำนวนดาวของโพสต์จากฐานข้อมูล

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);
});

ผู้ฟังจะได้รับ snapshot ที่มีข้อมูลในตำแหน่งที่ระบุในฐานข้อมูล ณ เวลาที่เกิดเหตุการณ์ คุณสามารถดึงข้อมูลใน snapshot โดยใช้เมธอด val()

อ่านข้อมูลเพียงครั้งเดียว

อ่านข้อมูลครั้งเดียวด้วย get()

SDK ออกแบบมาเพื่อจัดการการโต้ตอบกับเซิร์ฟเวอร์ฐานข้อมูล ไม่ว่าแอปจะออนไลน์หรือออฟไลน์ก็ตาม

โดยทั่วไป คุณควรใช้เทคนิคเหตุการณ์มูลค่าที่อธิบายไว้ข้างต้นในการอ่านข้อมูลเพื่อรับการแจ้งเตือนการอัปเดตข้อมูลจากแบ็กเอนด์ เทคนิค Listener จะลดการใช้งานและการเรียกเก็บเงินของคุณ และเพิ่มประสิทธิภาพเพื่อให้ผู้ใช้ได้รับประสบการณ์ที่ดีที่สุดเมื่อออนไลน์และออฟไลน์

หากต้องการข้อมูลเพียงครั้งเดียว คุณสามารถใช้ get() เพื่อรับภาพรวมของข้อมูลจากฐานข้อมูล หาก get() แสดงค่าเซิร์ฟเวอร์ไม่ได้ไม่ว่าด้วยเหตุผลใดก็ตาม ลูกค้าจะตรวจสอบแคชพื้นที่เก็บข้อมูลในเครื่องและแสดงข้อผิดพลาดหากยังคงไม่พบค่า

การใช้ get() โดยไม่จำเป็นอาจเพิ่มการใช้แบนด์วิดท์และทำให้ประสิทธิภาพการทำงานลดลง ซึ่งป้องกันได้โดยใช้ Listener แบบเรียลไทม์ดังที่แสดงด้านบน

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);
});

อ่านข้อมูล 1 ครั้งด้วยเครื่องมือตรวจสอบ

ในบางกรณี คุณอาจต้องการให้ระบบแสดงผลค่าจากแคชในเครื่องทันทีแทนที่จะตรวจสอบค่าที่อัปเดตแล้วในเซิร์ฟเวอร์ ในกรณีดังกล่าว คุณสามารถใช้ once() เพื่อรับข้อมูลจากแคชในดิสก์ในเครื่องได้ทันที

ซึ่งมีประโยชน์สําหรับข้อมูลที่จําเป็นต้องโหลดเพียงครั้งเดียวและคาดว่าจะไม่เปลี่ยนแปลงบ่อยครั้งหรือต้องมีการฟังอย่างสม่ำเสมอ ตัวอย่างเช่น แอปการเขียนบล็อกในตัวอย่างก่อนหน้านี้ใช้วิธีนี้เพื่อโหลดโปรไฟล์ของผู้ใช้เมื่อผู้ใช้เริ่มเขียนโพสต์ใหม่

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';
  // ...
});

การอัปเดตหรือลบข้อมูล

อัปเดตช่องที่เฉพาะเจาะจง

หากต้องการเขียนไปยังโหนดย่อยที่เฉพาะเจาะจงของโหนดพร้อมกันโดยไม่เขียนทับโหนดย่อยอื่นๆ ให้ใช้เมธอด update()

เมื่อเรียกใช้ update() คุณจะอัปเดตค่าย่อยระดับล่างได้โดยระบุเส้นทางสำหรับคีย์ หากจัดเก็บข้อมูลไว้ในหลายตำแหน่งเพื่อปรับขนาดได้ดียิ่งขึ้น คุณจะอัปเดตอินสแตนซ์ทั้งหมดของข้อมูลนั้นได้โดยใช้การแยกข้อมูล

ตัวอย่างเช่น แอปการเขียนบล็อกโซเชียลอาจสร้างโพสต์และอัปเดตโพสต์ไปยังฟีดกิจกรรมล่าสุดและฟีดกิจกรรมของผู้ใช้ที่โพสต์พร้อมกันโดยใช้โค้ดดังนี้

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);
}

ตัวอย่างนี้ใช้ push() เพื่อสร้างโพสต์ในโหนดที่มีโพสต์สําหรับผู้ใช้ทุกคนที่ /posts/$postid และดึงข้อมูลคีย์พร้อมกัน จากนั้นจะใช้คีย์ดังกล่าวเพื่อสร้างรายการที่ 2 ในโพสต์ของผู้ใช้ที่ /user-posts/$userid/$postid ได้

เมื่อใช้เส้นทางเหล่านี้ คุณจะอัปเดตตำแหน่งหลายแห่งในต้นไม้ JSON ได้พร้อมกันด้วยการเรียกใช้ update() ครั้งเดียว เช่น ตัวอย่างนี้สร้างโพสต์ใหม่ในทั้ง 2 ตำแหน่ง การอัปเดตพร้อมกันที่ทำด้วยวิธีนี้จะเป็นแบบ "ทั้งสำเร็จทั้งไม่สำเร็จ" กล่าวคือ การอัปเดตทั้งหมดจะสำเร็จหรือทั้งหมดจะล้มเหลว

เพิ่ม Callback ที่เสร็จสมบูรณ์

หากต้องการทราบว่ามีการบันทึกข้อมูลแล้วเมื่อใด คุณสามารถเพิ่มการเรียกกลับเมื่อเสร็จสมบูรณ์ได้ ทั้ง set() และ update() จะใช้การเรียกกลับเมื่อดำเนินการเสร็จสิ้นซึ่งไม่บังคับ ซึ่งจะเรียกใช้เมื่อมีการบันทึกลงในฐานข้อมูลแล้ว หากการเรียกไม่สําเร็จ ระบบจะส่งออบเจ็กต์ข้อผิดพลาดที่ระบุสาเหตุของการไม่สําเร็จไปยังการเรียกกลับ

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!
  }
});

ลบข้อมูล

วิธีที่ง่ายที่สุดในการลบข้อมูลคือการเรียกใช้ remove() ตามการอ้างอิงตำแหน่งของข้อมูลนั้น

นอกจากนี้ คุณยังลบได้โดยระบุ null เป็นค่าสําหรับการดําเนินการเขียนอื่น เช่น set() หรือ update() คุณสามารถใช้เทคนิคนี้กับ update() เพื่อลบรายการย่อยหลายรายการในการเรียก API เดียวได้

รับ Promise

หากต้องการทราบว่าระบบได้บันทึกข้อมูลของคุณลงในเซิร์ฟเวอร์ Firebase Realtime Database แล้วหรือยัง คุณจะใช้ Promise ได้ ทั้ง set() และ update() สามารถแสดงผล Promise ที่คุณสามารถใช้เพื่อดูว่ามีการเขียนข้อมูลไปยังฐานข้อมูลแล้วเมื่อใด

แยก Listener

ระบบจะนำการติดต่อกลับออกโดยการเรียกใช้เมธอด off() ในการอ้างอิงฐานข้อมูล Firebase

คุณนํา Listener รายการเดียวออกได้โดยส่งเป็นพารามิเตอร์ไปยัง off() การเรียกใช้ off() ในตำแหน่งที่ไม่มีอาร์กิวเมนต์จะนำ Listener ทั้งหมดในตำแหน่งนั้นออก

การเรียก off() ใน Listener หลักจะไม่นำ Listener ที่ลงทะเบียนในโหนดย่อยออกโดยอัตโนมัติ คุณต้องเรียก off() ใน Listener ย่อยด้วยเพื่อนำการเรียกกลับออก

บันทึกข้อมูลเป็นธุรกรรม

เมื่อทํางานกับข้อมูลที่อาจเสียหายจากการแก้ไขพร้อมกัน เช่น ตัวนับที่เพิ่มขึ้น คุณสามารถใช้การดำเนินการธุรกรรม คุณสามารถกําหนดฟังก์ชันการอัปเดตและฟังก์ชันการติดต่อกลับเมื่อเสร็จสิ้น (ไม่บังคับ) ให้กับการดำเนินการนี้ได้ ฟังก์ชันอัปเดตจะใช้สถานะปัจจุบันของข้อมูลเป็นอาร์กิวเมนต์และแสดงผลสถานะใหม่ที่ต้องการซึ่งคุณต้องการเขียน หากไคลเอ็นต์รายอื่นเขียนไปยังตำแหน่งก่อนที่จะเขียนค่าใหม่สำเร็จ ระบบจะเรียกใช้ฟังก์ชันการอัปเดตอีกครั้งด้วยค่าปัจจุบันใหม่ และพยายามเขียนอีกครั้ง

เช่น ในตัวอย่างแอปการเขียนบล็อกโซเชียล คุณสามารถอนุญาตให้ผู้ใช้ติดดาวและยกเลิกการติดดาวโพสต์ และติดตามจำนวนดาวที่โพสต์ได้รับ ดังนี้

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;
  });
}

การใช้ธุรกรรมจะช่วยป้องกันไม่ให้จำนวนดาวไม่ถูกต้องหากผู้ใช้หลายคนติดดาวโพสต์เดียวกันในเวลาเดียวกันหรือไคลเอ็นต์มีข้อมูลที่ล้าสมัย หากธุรกรรมถูกปฏิเสธ เซิร์ฟเวอร์จะแสดงผลค่าปัจจุบันไปยังไคลเอ็นต์ ซึ่งจะเรียกใช้ธุรกรรมอีกครั้งด้วยค่าที่อัปเดต การดำเนินการนี้จะซ้ำไปจนกว่าธุรกรรมจะได้รับการยอมรับหรือคุณยกเลิกธุรกรรม

การเพิ่มฝั่งเซิร์ฟเวอร์แบบอะตอม

ใน Use Case ข้างต้น เราจะเขียนค่า 2 ค่าลงในฐานข้อมูล ได้แก่ รหัสของผู้ใช้ที่กดชอบ/เลิกกดชอบโพสต์ และจำนวนการกดชอบที่เพิ่มขึ้น หากเราทราบอยู่แล้วว่าผู้ใช้ติดดาวโพสต์ เราสามารถใช้การดำเนินการการเพิ่มแบบอะตอมแทนธุรกรรมได้

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);
}

โค้ดนี้ไม่ได้ใช้การดำเนินการธุรกรรม ดังนั้นจึงไม่ทํางานอีกครั้งโดยอัตโนมัติหากมีการอัปเดตที่ขัดแย้งกัน แต่เนื่องจากการดำเนินการที่เพิ่มขึ้นนี้เกิดขึ้นบนเซิร์ฟเวอร์ฐานข้อมูลโดยตรง จึงไม่มีโอกาสเกิดข้อขัดแย้ง

หากต้องการตรวจหาและปฏิเสธความขัดแย้งเฉพาะแอปพลิเคชัน เช่น ผู้ใช้ติดดาวโพสต์ที่เคยติดดาวไว้แล้ว คุณควรเขียนกฎการรักษาความปลอดภัยที่กำหนดเองสำหรับกรณีการใช้งานนั้น

ใช้งานข้อมูลแบบออฟไลน์

หากลูกค้าขาดการเชื่อมต่อเครือข่าย แอปจะทำงานได้อย่างถูกต้องต่อไป

ไคลเอ็นต์ทุกรายที่เชื่อมต่อกับฐานข้อมูล Firebase จะดูแลรักษาเวอร์ชันภายในของตนเองสำหรับข้อมูลที่ใช้งานอยู่ เมื่อมีการเขียนข้อมูล ระบบจะเขียนข้อมูลลงในเวอร์ชันในเครื่องนี้ก่อน จากนั้นไคลเอ็นต์ Firebase จะซิงค์ข้อมูลดังกล่าวกับเซิร์ฟเวอร์ฐานข้อมูลระยะไกลและกับไคลเอ็นต์อื่นๆ โดยอิงตาม "ความพยายามที่ดีที่สุด"

ด้วยเหตุนี้ การเขียนทั้งหมดไปยังฐานข้อมูลจะทริกเกอร์เหตุการณ์ในเครื่องทันที ก่อนที่จะมีการเขียนข้อมูลไปยังเซิร์ฟเวอร์ ซึ่งหมายความว่าแอปจะยังคงตอบสนองอยู่เสมอ ไม่ว่าจะมีเวลาในการตอบสนองหรือการเชื่อมต่อเครือข่ายอย่างไรก็ตาม

เมื่อเชื่อมต่ออีกครั้ง แอปของคุณจะได้รับชุดเหตุการณ์ที่เหมาะสมเพื่อให้ไคลเอ็นต์ซิงค์กับสถานะเซิร์ฟเวอร์ปัจจุบันได้โดยไม่ต้องเขียนโค้ดที่กําหนดเอง

เราจะพูดถึงลักษณะการทํางานแบบออฟไลน์เพิ่มเติมในหัวข้อดูข้อมูลเพิ่มเติมเกี่ยวกับความสามารถแบบออนไลน์และออฟไลน์

ขั้นตอนถัดไป