(ไม่บังคับ) สร้างต้นแบบและทดสอบด้วย Firebase Emulator Suite
ก่อนจะพูดถึงวิธีที่แอปอ่านและเขียนข้อมูลจากฐานข้อมูลเรียลไทม์ เรามาแนะนำชุดเครื่องมือที่คุณใช้สร้างต้นแบบและทดสอบฟังก์ชันการทำงานของฐานข้อมูลเรียลไทม์ได้ นั่นก็คือ Firebase Emulator Suite หากคุณกำลังลองใช้โมเดลข้อมูลต่างๆ, ปรับกฎการรักษาความปลอดภัยให้เหมาะสม หรือพยายามหาวิธีโต้ตอบกับแบ็กเอนด์ที่คุ้มค่าที่สุด การทำงานในเครื่องโดยไม่ต้องติดตั้งใช้งานบริการจริงอาจเป็นความคิดที่ดี
โปรแกรมจำลองฐานข้อมูลเรียลไทม์เป็นส่วนหนึ่งของ Emulator Suite ซึ่งช่วยให้แอปโต้ตอบกับเนื้อหาและการกำหนดค่าฐานข้อมูลที่จำลอง รวมถึงทรัพยากรโปรเจ็กต์ที่จำลอง (ฟังก์ชัน ฐานข้อมูลอื่นๆ และกฎการรักษาความปลอดภัย) ได้ด้วยemulator_suite_short
การใช้โปรแกรมจำลองฐานข้อมูลเรียลไทม์มีขั้นตอนเพียงไม่กี่ขั้นตอน ดังนี้
- เพิ่มบรรทัดโค้ดลงในการกำหนดค่าการทดสอบของแอปเพื่อเชื่อมต่อกับโปรแกรมจำลอง
- เรียกใช้
firebase emulators:startจากรูทของไดเรกทอรีโปรเจ็กต์ในเครื่อง - เรียกใช้โค้ดต้นแบบของแอปโดยใช้ SDK ของแพลตฟอร์มฐานข้อมูลเรียลไทม์ตามปกติ หรือใช้ REST API ของฐานข้อมูลเรียลไทม์
มีคำแนะนำโดยละเอียดเกี่ยวกับการใช้ฐานข้อมูลเรียลไทม์และ Cloud Functions นอกจากนี้ คุณควรอ่านข้อมูลเบื้องต้นเกี่ยวกับ Emulator Suite ด้วย
รับ DatabaseReference
หากต้องการอ่านหรือเขียนข้อมูลจากฐานข้อมูล คุณต้องมีอินสแตนซ์ของ DatabaseReference ดังนี้
DatabaseReference ref = FirebaseDatabase.instance.ref();
เขียนข้อมูล
เอกสารนี้ครอบคลุมข้อมูลพื้นฐานเกี่ยวกับการอ่านและการเขียนข้อมูล Firebase
ระบบจะเขียนข้อมูล Firebase ลงใน DatabaseReference และดึงข้อมูลโดยรอหรือฟังเหตุการณ์ที่อ้างอิง ระบบจะส่งเหตุการณ์ 1 ครั้งสำหรับสถานะเริ่มต้นของข้อมูล และอีกครั้งทุกครั้งที่ข้อมูลมีการเปลี่ยนแปลง
การดำเนินการเขียนพื้นฐาน
สำหรับการดำเนินการเขียนพื้นฐาน คุณสามารถใช้ set() เพื่อบันทึกข้อมูลลงในการอ้างอิงที่ระบุ โดยจะแทนที่ข้อมูลที่มีอยู่ที่เส้นทางนั้น คุณสามารถตั้งค่าการอ้างอิงเป็นประเภทต่อไปนี้ได้ String, boolean, int, double, Map, List
ตัวอย่างเช่น คุณสามารถเพิ่มผู้ใช้ด้วย set() ได้ดังนี้
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
await ref.set({
"name": "John",
"age": 18,
"address": {
"line1": "100 Mountain View"
}
});
การใช้ set() ในลักษณะนี้จะเขียนทับข้อมูลในตำแหน่งที่ระบุ รวมถึงโหนดย่อย อย่างไรก็ตาม คุณยังคงอัปเดตโหนดย่อยได้โดยไม่ต้องเขียนออบเจ็กต์ทั้งหมดใหม่ หากต้องการอนุญาตให้ผู้ใช้อัปเดตโปรไฟล์ คุณสามารถอัปเดตชื่อผู้ใช้ได้ดังนี้
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Only update the age, leave the name and address!
await ref.update({
"age": 19,
});
เมธอด update() ยอมรับเส้นทางย่อยไปยังโหนด ซึ่งช่วยให้คุณอัปเดตโหนดหลายรายการในฐานข้อมูลได้พร้อมกัน
DatabaseReference ref = FirebaseDatabase.instance.ref("users");
await ref.update({
"123/age": 19,
"123/address/line1": "1 Mountain View",
});
อ่านข้อมูล
อ่านข้อมูลโดยการฟังเหตุการณ์ค่า
หากต้องการอ่านข้อมูลในเส้นทางและฟังการเปลี่ยนแปลง ให้ใช้พร็อพเพอร์ตี้ onValue ของ DatabaseReference เพื่อฟัง DatabaseEvent
คุณสามารถใช้ DatabaseEvent เพื่ออ่านข้อมูลในเส้นทางที่กำหนดตามข้อมูลที่มีอยู่ ณ เวลาที่เกิดเหตุการณ์ เหตุการณ์นี้จะทริกเกอร์ 1 ครั้งเมื่อแนบ Listener และอีกครั้งทุกครั้งที่ข้อมูลมีการเปลี่ยนแปลง รวมถึงข้อมูลย่อย เหตุการณ์มีพร็อพเพอร์ตี้ snapshot ที่มีข้อมูลทั้งหมดในตำแหน่งนั้น รวมถึงข้อมูลย่อย หากไม่มีข้อมูล พร็อพเพอร์ตี้ exists ของ Snapshot จะเป็น false และพร็อพเพอร์ตี้ value จะเป็น null
ตัวอย่างต่อไปนี้แสดงแอปพลิเคชันบล็อกโซเชียลที่ดึงรายละเอียดของโพสต์จากฐานข้อมูล
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
Listener จะได้รับ DataSnapshot ที่มีข้อมูลในตำแหน่งที่ระบุในฐานข้อมูล ณ เวลาที่เกิดเหตุการณ์ในพร็อพเพอร์ตี้ value
อ่านข้อมูล 1 ครั้ง
อ่าน 1 ครั้งโดยใช้ get()
SDK ได้รับการออกแบบมาเพื่อจัดการการโต้ตอบกับเซิร์ฟเวอร์ฐานข้อมูล ไม่ว่าแอปของคุณจะออนไลน์หรือออฟไลน์
โดยทั่วไป คุณควรใช้เทคนิคเหตุการณ์ค่าที่อธิบายไว้ข้างต้นเพื่ออ่านข้อมูลเพื่อรับการแจ้งเตือนเกี่ยวกับการอัปเดตข้อมูลจากแบ็กเอนด์ เทคนิคเหล่านี้จะช่วยลดการใช้งานและการเรียกเก็บเงิน รวมถึงได้รับการปรับให้เหมาะสมเพื่อมอบประสบการณ์การใช้งานที่ดีที่สุดแก่ผู้ใช้เมื่อออนไลน์และออฟไลน์
หากต้องการข้อมูลเพียงครั้งเดียว คุณสามารถใช้ get() เพื่อรับ Snapshot ของข้อมูลจากฐานข้อมูล หาก get() ไม่สามารถแสดงค่าเซิร์ฟเวอร์ได้ด้วยเหตุผลใดก็ตาม ไคลเอ็นต์จะตรวจสอบแคชพื้นที่เก็บข้อมูลในเครื่องและแสดงข้อผิดพลาดหากยังไม่พบค่า
ตัวอย่างต่อไปนี้แสดงการดึงชื่อผู้ใช้ที่แสดงต่อสาธารณะของผู้ใช้จากฐานข้อมูลเพียงครั้งเดียว
final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
print(snapshot.value);
} else {
print('No data available.');
}
การใช้ get() โดยไม่จำเป็นอาจเพิ่มการใช้แบนด์วิดท์และทำให้ประสิทธิภาพลดลง ซึ่งป้องกันได้โดยใช้ Listener แบบเรียลไทม์ตามที่แสดงไว้ข้างต้น
อ่านข้อมูล 1 ครั้งด้วย once()
ในบางกรณี คุณอาจต้องการให้ระบบแสดงค่าจากแคชในเครื่องทันทีแทนที่จะตรวจสอบค่าที่อัปเดตในเซิร์ฟเวอร์ ในกรณีดังกล่าว คุณสามารถใช้ once() เพื่อรับข้อมูลจากแคชดิสก์ในเครื่องได้ทันที
วิธีนี้มีประโยชน์สำหรับข้อมูลที่ต้องโหลดเพียงครั้งเดียวและไม่คาดว่าจะมีการเปลี่ยนแปลงบ่อยครั้งหรือต้องมีการฟังที่ใช้งานอยู่ ตัวอย่างเช่น แอปบล็อกในตัวอย่างก่อนหน้านี้ใช้วิธีนี้เพื่อโหลดโปรไฟล์ของผู้ใช้เมื่อเริ่มเขียนโพสต์ใหม่
final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';
การอัปเดตหรือลบข้อมูล
อัปเดตฟิลด์ที่เฉพาะเจาะจง
หากต้องการเขียนข้อมูลลงในข้อมูลย่อยที่เฉพาะเจาะจงของโหนดพร้อมกันโดยไม่เขียนทับโหนดย่อยอื่นๆ ให้ใช้เมธอด update()
เมื่อเรียกใช้ update() คุณสามารถอัปเดตค่าข้อมูลย่อยระดับล่างได้โดยการระบุเส้นทางสำหรับคีย์ หากจัดเก็บข้อมูลไว้ในหลายตำแหน่งเพื่อปรับขนาดให้ดีขึ้น คุณสามารถอัปเดตอินสแตนซ์ทั้งหมดของข้อมูลนั้นได้โดยใช้การกระจายข้อมูล ตัวอย่างเช่น แอปบล็อกโซเชียลอาจต้องการสร้างโพสต์และอัปเดตโพสต์นั้นลงในฟีดกิจกรรมล่าสุดและฟีดกิจกรรมของผู้ใช้ที่โพสต์พร้อมกัน แอปพลิเคชันบล็อกจะใช้โค้ดลักษณะนี้เพื่อดำเนินการดังกล่าว
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);
}
ตัวอย่างนี้ใช้ push() เพื่อสร้างโพสต์ในโหนดที่มีโพสต์ของผู้ใช้ทั้งหมดที่ /posts/$postid และดึงคีย์ด้วย key พร้อมกัน จากนั้นคุณสามารถใช้คีย์เพื่อสร้างรายการที่ 2 ในโพสต์ของผู้ใช้ที่ /user-posts/$userid/$postid
การใช้เส้นทางเหล่านี้ช่วยให้คุณอัปเดตตำแหน่งหลายตำแหน่งในแผนผัง JSON ได้พร้อมกันด้วยการเรียกใช้ update() เพียงครั้งเดียว เช่น วิธีที่ตัวอย่างนี้สร้างโพสต์ใหม่ในทั้ง 2 ตำแหน่ง การอัปเดตพร้อมกันที่ทำในลักษณะนี้จะเป็นแบบอะตอมมิก ซึ่งหมายความว่าการอัปเดตทั้งหมดจะสำเร็จหรือล้มเหลวทั้งหมด
เพิ่มการเรียกกลับการดำเนินการให้เสร็จสมบูรณ์
หากต้องการทราบว่าข้อมูลได้รับการคอมมิตแล้วเมื่อใด คุณสามารถลงทะเบียนการเรียกกลับการดำเนินการให้เสร็จสมบูรณ์ได้ ทั้ง set() และ update() จะแสดงผล Future ซึ่งคุณสามารถแนบการเรียกกลับความสำเร็จและข้อผิดพลาดที่จะเรียกใช้เมื่อมีการคอมมิตการเขียนลงในฐานข้อมูลและเมื่อการเรียกใช้ไม่สำเร็จ
FirebaseDatabase.instance
.ref('users/$userId/email')
.set(emailAddress)
.then((_) {
// Data saved successfully!
})
.catchError((error) {
// The write failed...
});
ลบข้อมูล
วิธีที่ง่ายที่สุดในการลบข้อมูลคือการเรียกใช้ remove() ในการอ้างอิงไปยังตำแหน่งของข้อมูลนั้น
นอกจากนี้ คุณยังลบข้อมูลได้โดยการระบุค่า null สำหรับการดำเนินการเขียนอื่นๆ เช่น set() หรือ update() คุณสามารถใช้เทคนิคนี้กับ update() เพื่อลบข้อมูลย่อยหลายรายการในการเรียกใช้ API ครั้งเดียว
บันทึกข้อมูลเป็นธุรกรรม
เมื่อทำงานกับข้อมูลที่อาจเสียหายจากการแก้ไขพร้อมกัน
เช่น ตัวนับแบบเพิ่มค่า คุณสามารถใช้ธุรกรรมได้โดยส่งผ่าน
แฮนเดิลอร์ธุรกรรมไปยัง runTransaction() แฮนเดิลอร์ธุรกรรมจะใช้สถานะปัจจุบันของข้อมูลเป็นอาร์กิวเมนต์และแสดงสถานะใหม่ที่ต้องการเขียน หากไคลเอ็นต์อื่นเขียนข้อมูลลงในตำแหน่งก่อนที่จะเขียนค่าใหม่สำเร็จ ระบบจะเรียกใช้ฟังก์ชันการอัปเดตอีกครั้งด้วยค่าปัจจุบันใหม่ และลองเขียนอีกครั้ง
ตัวอย่างเช่น ในแอปบล็อกโซเชียล คุณสามารถอนุญาตให้ผู้ใช้ติดดาวและเลิกติดดาวโพสต์ รวมถึงติดตามจำนวนดาวที่โพสต์ได้รับได้ดังนี้
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);
});
}
โดยค่าเริ่มต้น ระบบจะแสดงเหตุการณ์ทุกครั้งที่ฟังก์ชันการอัปเดตธุรกรรมทำงาน ดังนั้นหากคุณเรียกใช้ฟังก์ชันหลายครั้ง คุณอาจเห็นสถานะระดับกลาง
คุณสามารถตั้งค่า applyLocally เป็น false เพื่อระงับสถานะระดับกลางเหล่านี้และรอจนกว่าธุรกรรมจะเสร็จสมบูรณ์ก่อนที่จะแสดงเหตุการณ์
await ref.runTransaction((Object? post) {
// ...
}, applyLocally: false);
ผลลัพธ์ของธุรกรรมคือ TransactionResult ซึ่งมีข้อมูล เช่น มีการคอมมิตธุรกรรมหรือไม่ และ Snapshot ใหม่
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
การยกเลิกธุรกรรม
หากต้องการยกเลิกธุรกรรมอย่างปลอดภัย ให้เรียกใช้ Transaction.abort() เพื่อ
แสดง AbortTransactionException
TransactionResult result = await ref.runTransaction((Object? user) {
if (user !== null) {
return Transaction.abort();
}
// ...
});
print(result.committed); // false
การเพิ่มค่าฝั่งเซิร์ฟเวอร์แบบอะตอมมิก
ในกรณีการใช้งานข้างต้น เรากำลังเขียนค่า 2 ค่าลงในฐานข้อมูล ได้แก่ รหัสของผู้ใช้ที่ติดดาว/เลิกติดดาวโพสต์ และจำนวนดาวที่เพิ่มขึ้น หากทราบอยู่แล้วว่าผู้ใช้กำลังติดดาวโพสต์ เราสามารถใช้การดำเนินการเพิ่มค่าแบบอะตอมมิกแทนธุรกรรมได้
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);
}
โค้ดนี้ไม่ได้ใช้การดำเนินการธุรกรรม ดังนั้นระบบจะไม่เรียกใช้โค้ดนี้อีกครั้งโดยอัตโนมัติหากมีการอัปเดตที่ขัดแย้งกัน อย่างไรก็ตาม เนื่องจากมีการดำเนินการเพิ่มค่าในเซิร์ฟเวอร์ฐานข้อมูลโดยตรง จึงไม่มีโอกาสที่จะเกิดความขัดแย้ง
หากต้องการตรวจหาและปฏิเสธความขัดแย้งที่เฉพาะเจาะจงกับแอปพลิเคชัน เช่น ผู้ใช้ติดดาวโพสต์ที่เคยติดดาวไปแล้ว คุณควรเขียนกฎการรักษาความปลอดภัยที่กำหนดเองสำหรับกรณีการใช้งานนั้น
ทำงานกับข้อมูลแบบออฟไลน์
หากไคลเอ็นต์สูญเสียการเชื่อมต่อเครือข่าย แอปของคุณจะยังคงทำงานได้อย่างถูกต้อง
ไคลเอ็นต์ทุกตัวที่เชื่อมต่อกับฐานข้อมูล Firebase จะเก็บรักษาข้อมูลเวอร์ชันภายในของตัวเองสำหรับข้อมูลที่ใช้งานอยู่ เมื่อมีการเขียนข้อมูล ระบบจะเขียนข้อมูลลงในเวอร์ชันในเครื่องนี้ก่อน จากนั้นไคลเอ็นต์ Firebase จะซิงโครไนซ์ข้อมูลนั้นกับเซิร์ฟเวอร์ฐานข้อมูลระยะไกลและกับไคลเอ็นต์อื่นๆ ตามความพยายามอย่างเต็มที่
ด้วยเหตุนี้ การเขียนข้อมูลทั้งหมดลงในฐานข้อมูลจะทริกเกอร์เหตุการณ์ในเครื่องทันที ก่อนที่จะเขียนข้อมูลลงในเซิร์ฟเวอร์ ซึ่งหมายความว่าแอปของคุณจะยังคงตอบสนองได้ไม่ว่าจะมีเวลาในการรับส่งข้อมูลผ่านเครือข่ายหรือการเชื่อมต่อเป็นอย่างไร
เมื่อมีการเชื่อมต่ออีกครั้ง แอปของคุณจะได้รับชุดเหตุการณ์ที่เหมาะสมเพื่อให้ไคลเอ็นต์ซิงค์กับสถานะเซิร์ฟเวอร์ปัจจุบันโดยไม่ต้องเขียนโค้ดที่กำหนดเอง
เราจะพูดถึงลักษณะการทำงานแบบออฟไลน์เพิ่มเติมใน ดูข้อมูลเพิ่มเติมเกี่ยวกับความสามารถแบบออนไลน์และออฟไลน์