(ไม่บังคับ) สร้างต้นแบบและทดสอบด้วย Firebase Emulator Suite
ก่อนจะพูดถึงวิธีที่แอปอ่านและเขียนไปยัง Realtime Database เรามาทำความรู้จักชุดเครื่องมือที่คุณใช้สร้างต้นแบบและทดสอบฟังก์ชันการทำงานของ Realtime Database ได้กันก่อน นั่นคือ Firebase Emulator Suite หากคุณกำลังทดลองใช้โมเดลข้อมูลต่างๆ เพิ่มประสิทธิภาพกฎความปลอดภัย หรือพยายามหาวิธีโต้ตอบกับแบ็กเอนด์ที่คุ้มค่าที่สุด การทำงานในเครื่องโดยไม่ต้องติดตั้งใช้งานบริการจริงอาจเป็นไอเดียที่ดี
โปรแกรมจำลอง Realtime Database เป็นส่วนหนึ่งของชุดโปรแกรมจำลอง ซึ่งช่วยให้แอปโต้ตอบกับเนื้อหาและการกำหนดค่าฐานข้อมูลจำลอง รวมถึงทรัพยากรโปรเจ็กต์จำลอง (ฟังก์ชัน ฐานข้อมูลอื่นๆ และกฎการรักษาความปลอดภัย) ได้ด้วย (ไม่บังคับ)emulator_suite_short
การใช้โปรแกรมจำลอง Realtime Database มีขั้นตอนเพียงไม่กี่ขั้นตอน ดังนี้
- การเพิ่มบรรทัดโค้ดลงในการกำหนดค่าการทดสอบของแอปเพื่อเชื่อมต่อกับโปรแกรมจำลอง
- จากรูทของไดเรกทอรีโปรเจ็กต์ที่อยู่ในเครื่อง ให้เรียกใช้
firebase emulators:start - การโทรจากโค้ดต้นแบบของแอปโดยใช้ SDK ของแพลตฟอร์ม Realtime Database ตามปกติ หรือใช้ REST API ของ Realtime Database
มีคำแนะนำแบบทีละขั้นตอนโดยละเอียดที่เกี่ยวข้องกับ Realtime Database และ Cloud Functions นอกจากนี้ คุณควรอ่านข้อมูลเบื้องต้นเกี่ยวกับชุดโปรแกรมจำลองด้วย
รับ DatabaseReference
หากต้องการอ่านหรือเขียนข้อมูลจากฐานข้อมูล คุณต้องมีอินสแตนซ์ของ
DatabaseReference
DatabaseReference ref = FirebaseDatabase.instance.ref();
เขียนข้อมูล
เอกสารนี้ครอบคลุมข้อมูลพื้นฐานเกี่ยวกับการอ่านและการเขียนข้อมูล Firebase
ระบบจะเขียนข้อมูล Firebase ไปยัง DatabaseReference และเรียกข้อมูลโดย
รอหรือฟังเหตุการณ์ที่อ้างอิงปล่อยออกมา ระบบจะปล่อยเหตุการณ์
หนึ่งครั้งสำหรับสถานะเริ่มต้นของข้อมูล และอีกครั้งเมื่อใดก็ตามที่ข้อมูลเปลี่ยนแปลง
การดำเนินการเขียนพื้นฐาน
สำหรับการดำเนินการเขียนพื้นฐาน คุณสามารถใช้ 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 ของสแนปชอตจะเป็น false และพร็อพเพอร์ตี้ value จะเป็น Null
ตัวอย่างต่อไปนี้แสดงแอปพลิเคชันบล็อกทางสังคมที่ดึงรายละเอียดของโพสต์จากฐานข้อมูล
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
ผู้ฟังจะได้รับ DataSnapshot ซึ่งมีข้อมูลในตำแหน่งที่ระบุ
ในฐานข้อมูล ณ เวลาที่เกิดเหตุการณ์ในพร็อพเพอร์ตี้ value
อ่านข้อมูลครั้งเดียว
อ่านครั้งเดียวโดยใช้ get()
SDK ได้รับการออกแบบมาเพื่อจัดการการโต้ตอบกับเซิร์ฟเวอร์ฐานข้อมูลไม่ว่าแอปจะออนไลน์หรือออฟไลน์ก็ตาม
โดยทั่วไป คุณควรใช้เทคนิคเหตุการณ์ค่าที่อธิบายไว้ข้างต้นเพื่ออ่าน ข้อมูลเพื่อรับการแจ้งเตือนเกี่ยวกับการอัปเดตข้อมูลจากแบ็กเอนด์ เทคนิคเหล่านี้จะช่วยลดการใช้งานและการเรียกเก็บเงินของคุณ และได้รับการเพิ่มประสิทธิภาพเพื่อให้ผู้ใช้ได้รับประสบการณ์ที่ดีที่สุดทั้งเมื่อออนไลน์และออฟไลน์
หากต้องการข้อมูลเพียงครั้งเดียว คุณสามารถใช้ get() เพื่อรับข้อมูลสแนปชอตจากฐานข้อมูลได้
หาก get() ไม่สามารถแสดงค่าเซิร์ฟเวอร์ได้ไม่ว่าจะด้วยเหตุผลใดก็ตาม
ไคลเอ็นต์จะตรวจสอบแคชในพื้นที่เก็บข้อมูลและแสดงข้อผิดพลาด
หากยังไม่พบค่า
ตัวอย่างต่อไปนี้แสดงการดึงชื่อผู้ใช้ที่แสดงต่อสาธารณะของ ผู้ใช้ 1 ครั้งจากฐานข้อมูล
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() โดยไม่จำเป็นอาจเพิ่มการใช้แบนด์วิดท์และทำให้ประสิทธิภาพลดลง
ซึ่งป้องกันได้โดยใช้เครื่องมือฟังแบบเรียลไทม์ตามที่แสดง
ด้านบน
อ่านข้อมูลครั้งเดียวด้วย once()
ในบางกรณี คุณอาจต้องการให้ระบบแสดงค่าจากแคชในเครื่องทันทีแทนที่จะตรวจสอบค่าที่อัปเดตแล้วในเซิร์ฟเวอร์ ในกรณีดังกล่าว คุณสามารถใช้ once() เพื่อรับข้อมูลจากแคชดิสก์ในเครื่องได้ทันที
ซึ่งมีประโยชน์สำหรับข้อมูลที่ต้องโหลดเพียงครั้งเดียวและไม่คาดว่าจะ มีการเปลี่ยนแปลงบ่อยครั้งหรือต้องมีการฟังอย่างต่อเนื่อง ตัวอย่างเช่น แอปบล็อก ในตัวอย่างก่อนหน้าใช้วิธีนี้เพื่อโหลดโปรไฟล์ของผู้ใช้เมื่อผู้ใช้ เริ่มเขียนโพสต์ใหม่
final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';
การอัปเดตหรือลบข้อมูล
อัปเดตฟิลด์ที่เฉพาะเจาะจง
หากต้องการเขียนไปยังโหนดย่อยที่เฉพาะเจาะจงของโหนดพร้อมกันโดยไม่เขียนทับโหนดย่อยอื่นๆ
ให้ใช้เมธอด update()
เมื่อเรียกใช้ update() คุณจะอัปเดตค่าของรายการย่อยระดับล่างได้โดย
ระบุเส้นทางสำหรับคีย์ หากจัดเก็บข้อมูลไว้ในหลายตำแหน่งเพื่อปรับขนาดให้ดีขึ้น คุณสามารถอัปเดตอินสแตนซ์ทั้งหมดของข้อมูลนั้นได้โดยใช้แฟนเอาต์ (Fan-Out) ตัวอย่างเช่น แอปบล็อกโซเชียลอาจต้องการสร้างโพสต์และอัปเดตโพสต์พร้อมกันไปยังฟีดกิจกรรมล่าสุดและฟีดกิจกรรมของผู้ใช้ที่โพสต์ โดย
แอปพลิเคชันบล็อกจะใช้โค้ดต่อไปนี้
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 ซึ่งมีข้อมูล เช่น
ธุรกรรมได้รับการยืนยันหรือไม่ และภาพรวมใหม่
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 จะซิงโครไนซ์ข้อมูลดังกล่าวกับเซิร์ฟเวอร์ฐานข้อมูลระยะไกล และกับไคลเอ็นต์อื่นๆ โดยพยายามอย่างเต็มที่
ด้วยเหตุนี้ การเขียนทั้งหมดลงในฐานข้อมูลจึงทําให้เกิดเหตุการณ์ในเครื่องทันที ก่อนที่จะเขียนข้อมูลใดๆ ลงในเซิร์ฟเวอร์ ซึ่งหมายความว่าแอปจะยังคงตอบสนองได้ไม่ว่าเครือข่ายจะมีเวลาในการตอบสนองหรือการเชื่อมต่อเป็นอย่างไร
เมื่อมีการเชื่อมต่ออีกครั้ง แอปจะได้รับชุดเหตุการณ์ที่เหมาะสมเพื่อให้ไคลเอ็นต์ซิงค์กับสถานะเซิร์ฟเวอร์ปัจจุบันโดยไม่ต้องเขียนโค้ดที่กำหนดเอง
เราจะพูดถึงพฤติกรรมออฟไลน์เพิ่มเติมในส่วนดูข้อมูลเพิ่มเติมเกี่ยวกับความสามารถออนไลน์และออฟไลน์