(ไม่บังคับ) สร้างต้นแบบและทดสอบด้วย 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 และเรียกข้อมูลโดย
รอหรือฟังเหตุการณ์ที่อ้างอิงปล่อยออกมา ระบบจะปล่อยเหตุการณ์
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 ของสแนปชอตจะเป็น 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() โดยไม่จำเป็นอาจเพิ่มการใช้แบนด์วิดท์และทำให้ประสิทธิภาพลดลง ซึ่งป้องกันได้โดยใช้ Listener แบบเรียลไทม์ตามที่แสดง ด้านบน
อ่านข้อมูลครั้งเดียวด้วย 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() จะแสดงผล Futures ซึ่งคุณสามารถแนบการเรียกกลับที่สำเร็จและข้อผิดพลาดได้ ซึ่งจะเรียกใช้เมื่อมีการคอมมิตการเขียนไปยังฐานข้อมูลและเมื่อการเรียกไม่สำเร็จ
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 จะซิงโครไนซ์ข้อมูลดังกล่าวกับเซิร์ฟเวอร์ฐานข้อมูลระยะไกล และกับไคลเอ็นต์อื่นๆ โดยพยายามอย่างเต็มที่
ด้วยเหตุนี้ การเขียนทั้งหมดไปยังฐานข้อมูลจึงทําให้เกิดเหตุการณ์ในเครื่องทันที ก่อนที่จะมีการเขียนข้อมูลใดๆ ไปยังเซิร์ฟเวอร์ ซึ่งหมายความว่าแอปจะยังคงตอบสนองได้ไม่ว่าเครือข่ายจะมีเวลาในการตอบสนองหรือการเชื่อมต่อเป็นอย่างไร
เมื่อมีการเชื่อมต่ออีกครั้ง แอปจะได้รับชุดเหตุการณ์ที่เหมาะสมเพื่อให้ไคลเอ็นต์ซิงค์กับสถานะเซิร์ฟเวอร์ปัจจุบันโดยไม่ต้องเขียนโค้ดที่กำหนดเอง
เราจะพูดถึงพฤติกรรมออฟไลน์เพิ่มเติมในส่วนดูข้อมูลเพิ่มเติมเกี่ยวกับความสามารถออนไลน์และออฟไลน์