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

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

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

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

การใช้ Realtime Database emulator มีขั้นตอนเพียงไม่กี่ขั้นตอน ดังนี้

  1. เพิ่มบรรทัดโค้ดลงในการกำหนดค่าการทดสอบของแอปเพื่อเชื่อมต่อกับโปรแกรมจำลอง
  2. เรียกใช้ firebase emulators:start จากรูทของไดเรกทอรีโปรเจ็กต์ที่อยู่ในเครื่อง
  3. เรียกใช้โค้ดต้นแบบของแอปโดยใช้ 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 ของ 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 จะซิงโครไนซ์ข้อมูลนั้นกับเซิร์ฟเวอร์ฐานข้อมูลระยะไกลและกับไคลเอ็นต์อื่นๆ ตามความพยายามอย่างเต็มที่

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

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

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

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