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

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

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

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

การใช้โปรแกรมจำลอง Realtime Database มีขั้นตอนเพียงไม่กี่ขั้นตอน ดังนี้

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

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

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

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

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