(ไม่บังคับ) สร้างต้นแบบและทดสอบด้วย Firebase Local Emulator Suite
ก่อนจะพูดถึงวิธีที่แอปอ่านและเขียนลงใน Realtime Database เราขอแนะนำชุดเครื่องมือที่คุณสามารถใช้สร้างต้นแบบและทดสอบฟังก์ชันการทำงานของ Realtime Database ดังนี้ Firebase Local Emulator Suite หากคุณกำลังลองใช้รูปแบบข้อมูลต่างๆ เพิ่มประสิทธิภาพกฎความปลอดภัย หรือพยายามหาวิธีที่คุ้มค่าที่สุดในการโต้ตอบกับแบ็กเอนด์ ความสามารถในการทํางานในเครื่องโดยไม่ต้องทําให้บริการใช้งานได้จริงอาจเป็นแนวคิดที่ยอดเยี่ยม
โปรแกรมจำลอง Realtime Database เป็นส่วนหนึ่งของ Local Emulator Suite ซึ่งช่วยให้แอปโต้ตอบกับเนื้อหาและการกำหนดค่าฐานข้อมูลที่จำลอง รวมถึงทรัพยากรโปรเจ็กต์ที่จำลอง (ฟังก์ชัน ฐานข้อมูลอื่นๆ และกฎการรักษาความปลอดภัย) ได้ด้วยหากต้องการ
การใช้โปรแกรมจำลอง Realtime Database มีเพียงไม่กี่ขั้นตอนดังนี้
- การเพิ่มโค้ด 1 บรรทัดลงในการกําหนดค่าการทดสอบของแอปเพื่อเชื่อมต่อกับโปรแกรมจําลอง
- จากรูทของไดเรกทอรีโปรเจ็กต์ในเครื่อง ให้เรียกใช้
firebase emulators:start
- การเรียกใช้จากโค้ดโปรโตไทป์ของแอปโดยใช้ 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
โปรแกรมฟังจะทริกเกอร์ 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 ออกแบบมาเพื่อจัดการการโต้ตอบกับเซิร์ฟเวอร์ฐานข้อมูล ไม่ว่าแอปจะออนไลน์หรือออฟไลน์ก็ตาม
โดยทั่วไป คุณควรใช้เทคนิคเหตุการณ์ที่มีค่าที่อธิบายไว้ข้างต้นเพื่ออ่านข้อมูลเพื่อรับการแจ้งเตือนการอัปเดตข้อมูลจากแบ็กเอนด์ เทคนิคเหล่านี้จะช่วยลดการใช้งานและการเรียกเก็บเงิน และได้รับการเพิ่มประสิทธิภาพเพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดีที่สุดเมื่อออนไลน์และออฟไลน์
หากต้องการข้อมูลเพียงครั้งเดียว คุณสามารถใช้ 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 ตำแหน่ง การอัปเดตพร้อมกันที่ทำด้วยวิธีนี้จะเป็นแบบ "ทั้งสำเร็จทั้งไม่สำเร็จ" กล่าวคือ การอัปเดตทั้งหมดจะสำเร็จหรือทั้งหมดจะล้มเหลว
เพิ่มการเรียกกลับเมื่อดำเนินการเสร็จสมบูรณ์
หากต้องการทราบว่ามีการบันทึกข้อมูลแล้วเมื่อใด คุณสามารถเพิ่มการเรียกกลับเมื่อเสร็จสมบูรณ์ได้ ทั้ง 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()
ในตำแหน่งโดยไม่มีอาร์กิวเมนต์จะนำผู้ฟังทั้งหมดในตำแหน่งนั้นออก
การเรียก 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 จะซิงค์ข้อมูลดังกล่าวกับเซิร์ฟเวอร์ฐานข้อมูลระยะไกลและกับไคลเอ็นต์อื่นๆ โดยอิงตาม "ความพยายามที่ดีที่สุด"
ด้วยเหตุนี้ การเขียนทั้งหมดในฐานข้อมูลจึงทริกเกอร์เหตุการณ์ในเครื่องทันทีก่อนที่จะมีการเขียนข้อมูลไปยังเซิร์ฟเวอร์ ซึ่งหมายความว่าแอปจะยังคงตอบสนองอยู่เสมอ ไม่ว่าจะมีเวลาในการตอบสนองหรือการเชื่อมต่อเครือข่ายอย่างไรก็ตาม
เมื่อเชื่อมต่ออีกครั้ง แอปของคุณจะได้รับชุดเหตุการณ์ที่เหมาะสมเพื่อให้ไคลเอ็นต์ซิงค์กับสถานะเซิร์ฟเวอร์ปัจจุบันได้โดยไม่ต้องเขียนโค้ดที่กําหนดเอง
เราจะพูดถึงลักษณะการทํางานแบบออฟไลน์เพิ่มเติมในหัวข้อดูข้อมูลเพิ่มเติมเกี่ยวกับความสามารถแบบออนไลน์และออฟไลน์