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

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

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

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

การใช้โปรแกรมจำลองฐานข้อมูลเรียลไทม์เกี่ยวข้องกับขั้นตอนเพียงไม่กี่ขั้นตอน:

  1. การเพิ่มบรรทัดโค้ดในการกำหนดค่าการทดสอบของแอปเพื่อเชื่อมต่อกับโปรแกรมจำลอง
  2. จากรูทของไดเร็กทอรีโปรเจ็กต์ในเครื่องของคุณ ให้รัน firebase emulators:start
  3. การโทรจากโค้ดต้นแบบของแอปโดยใช้ SDK แพลตฟอร์ม Realtime Database ตามปกติ หรือใช้ Realtime Database REST API

มี คำแนะนำโดยละเอียดเกี่ยวกับ Realtime Database และ Cloud Functions คุณควรดู บทแนะนำ Emulator Suite ด้วย

รับฐานข้อมูลอ้างอิง

หากต้องการอ่านหรือเขียนข้อมูลจากฐานข้อมูล คุณต้องมีอินสแตนซ์ของ 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 name, leave the age 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 เพื่ออ่านข้อมูลในเส้นทางที่กำหนด เนื่องจากมีอยู่ในเวลาที่เกิดเหตุการณ์ เหตุการณ์นี้จะเกิดขึ้นครั้งเดียวเมื่อมีการแนบผู้ฟังและอีกครั้งทุกครั้งที่มีการเปลี่ยนแปลงข้อมูล รวมถึงรายการย่อย เหตุการณ์มีคุณสมบัติส snapshot อตที่มีข้อมูลทั้งหมดในสถานที่นั้น รวมถึงข้อมูลย่อย หากไม่มีข้อมูล คุณสมบัติที่ exists ของสแน็ปช็อตจะเป็น false และคุณสมบัติของ value จะเป็นโมฆะ

ตัวอย่างต่อไปนี้สาธิตแอปพลิเคชันบล็อกโซเชียลที่ดึงรายละเอียดของโพสต์จากฐานข้อมูล:

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() ไม่สามารถคืนค่าเซิร์ฟเวอร์ได้ ไคลเอ็นต์จะตรวจสอบแคชที่จัดเก็บในตัวเครื่องและส่งคืนข้อผิดพลาดหากไม่พบค่าดังกล่าว

ตัวอย่างต่อไปนี้สาธิตการดึงชื่อผู้ใช้ที่เปิดเผยต่อสาธารณะของผู้ใช้ในครั้งเดียวจากฐานข้อมูล:

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() เพื่อรับข้อมูลจากแคชดิสก์ในเครื่องทันที

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

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

กำลังอัปเดตหรือลบข้อมูล

อัพเดทเฉพาะช่อง

หากต้องการเขียนไปยังโหนดย่อยที่ระบุพร้อมกันโดยไม่เขียนทับโหนดย่อยอื่น ให้ใช้เมธอด update()

เมื่อเรียกใช้ update() คุณสามารถอัปเดตค่าลูกระดับล่างโดยระบุเส้นทางสำหรับคีย์ หากข้อมูลถูกจัดเก็บไว้ในหลายตำแหน่งเพื่อให้ปรับขนาดได้ดีขึ้น คุณสามารถอัปเดตอินสแตนซ์ทั้งหมดของข้อมูลนั้นได้โดยใช้ data 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 คีย์สามารถใช้เพื่อสร้างรายการที่สองในโพสต์ของผู้ใช้ที่ /user-posts/$userid/$postid

เมื่อใช้พาธเหล่านี้ คุณสามารถทำการอัปเดตหลายตำแหน่งพร้อมกันในแผนผัง JSON ได้ด้วยการเรียก update() เพียงครั้งเดียว เช่น วิธีที่ตัวอย่างนี้สร้างโพสต์ใหม่ในทั้งสองตำแหน่ง การอัปเดตพร้อมกันในลักษณะนี้เป็นแบบปรมาณู: การอัปเดตทั้งหมดสำเร็จหรือการอัปเดตทั้งหมดล้มเหลว

เพิ่มการโทรกลับที่เสร็จสิ้น

หากคุณต้องการทราบว่าข้อมูลของคุณได้รับมอบหมายเมื่อใด คุณสามารถลงทะเบียนการโทรกลับเพื่อให้เสร็จสิ้นได้ ทั้ง 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

Atomic Server-side เพิ่มขึ้น

ในกรณีการใช้งานข้างต้น เรากำลังเขียนค่าสองค่าลงในฐานข้อมูล: ID ของผู้ใช้ที่ติดดาว/เลิกติดดาวโพสต์ และจำนวนดาวที่เพิ่มขึ้น หากเราทราบแล้วว่าผู้ใช้กำลังติดดาวโพสต์ เราสามารถใช้การดำเนินการเพิ่มทีละอะตอมแทนธุรกรรมได้

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

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

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

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

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