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

เอกสารนี้ครอบคลุมพื้นฐานการอ่านและการเขียนข้อมูล Firebase

ข้อมูล Firebase จะเขียนลงในข้อมูลอ้างอิง FirebaseDatabase และดึงข้อมูลโดยการแนบ Listener แบบไม่พร้อมกันเข้ากับข้อมูลอ้างอิง Listener จะทริกเกอร์ 1 ครั้งเพื่อแสดงสถานะเริ่มต้นของข้อมูล และอีกครั้งเมื่อข้อมูลมีการเปลี่ยนแปลง

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

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

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

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

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

โปรดดูคำแนะนำแบบทีละขั้นเกี่ยวกับ Realtime Database และ Cloud Functions โดยละเอียด นอกจากนี้คุณควรดูข้อมูลที่ข้อมูลเบื้องต้นเกี่ยวกับชุดโปรแกรมจำลองในเครื่องด้วย

รับ DatabaseReference

หากต้องการอ่านหรือเขียนข้อมูลจากฐานข้อมูล คุณต้องมีอินสแตนซ์ของ DatabaseReference

Kotlin+KTX

private lateinit var database: DatabaseReference
// ...
database = Firebase.database.reference

Java

private DatabaseReference mDatabase;
// ...
mDatabase = FirebaseDatabase.getInstance().getReference();

เขียนข้อมูล

การดำเนินการเขียนพื้นฐาน

สำหรับการดำเนินการเขียนพื้นฐาน คุณจะใช้ setValue() เพื่อบันทึกข้อมูลลงในการอ้างอิงที่ระบุได้ โดยแทนที่ข้อมูลที่มีอยู่ในเส้นทางนั้น คุณใช้วิธีการนี้เพื่อทำสิ่งต่อไปนี้ได้

  • ประเภทบัตรที่สอดคล้องกับประเภท JSON ที่ใช้ได้มีดังนี้
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • ส่งออบเจ็กต์ Java ที่กำหนดเอง หากคลาสที่กำหนดมีตัวสร้างเริ่มต้นที่ไม่มีอาร์กิวเมนต์และมี Getter สาธารณะสำหรับการกำหนดพร็อพเพอร์ตี้

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

Kotlin+KTX

@IgnoreExtraProperties
data class User(val username: String? = null, val email: String? = null) {
    // Null default values create a no-argument default constructor, which is needed
    // for deserialization from a DataSnapshot.
}

Java

@IgnoreExtraProperties
public class User {

    public String username;
    public String email;

    public User() {
        // Default constructor required for calls to DataSnapshot.getValue(User.class)
    }

    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }

}

คุณเพิ่มผู้ใช้ที่มี setValue() ได้โดยทำดังนี้

Kotlin+KTX

fun writeNewUser(userId: String, name: String, email: String) {
    val user = User(name, email)

    database.child("users").child(userId).setValue(user)
}

Java

public void writeNewUser(String userId, String name, String email) {
    User user = new User(name, email);

    mDatabase.child("users").child(userId).setValue(user);
}

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

Kotlin+KTX

database.child("users").child(userId).child("username").setValue(name)

Java

mDatabase.child("users").child(userId).child("username").setValue(name);

อ่านข้อมูล

อ่านข้อมูลด้วย Listener แบบถาวร

หากต้องการอ่านข้อมูลที่เส้นทางและฟังการเปลี่ยนแปลง ให้ใช้เมธอด addValueEventListener() เพื่อเพิ่ม ValueEventListener ไปยัง DatabaseReference

การส่งแบบฟอร์ม Callback ของเหตุการณ์ การใช้งานทั่วไป
ValueEventListener onDataChange() อ่านและรับฟังการเปลี่ยนแปลงเนื้อหาทั้งหมดของเส้นทาง

คุณใช้เมธอด onDataChange() เพื่ออ่านสแนปชอตแบบคงที่ของเนื้อหาในเส้นทางที่ระบุได้ ตามที่มีอยู่ในขณะที่เกิดเหตุการณ์ ระบบจะทริกเกอร์เมธอดนี้ 1 ครั้งเมื่อมีการแนบ Listener และอีกครั้งทุกครั้งที่ข้อมูลรวมถึงเด็กด้วย มีการเปลี่ยนแปลง Callback ของเหตุการณ์จะส่งผ่านสแนปชอตที่มีข้อมูลในตำแหน่งดังกล่าว รวมถึงข้อมูลย่อย หากไม่มีข้อมูล สแนปชอตจะแสดง false เมื่อคุณเรียกใช้ exists() และ null เมื่อคุณเรียกใช้ getValue()

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

Kotlin+KTX

val postListener = object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        // Get Post object and use the values to update the UI
        val post = dataSnapshot.getValue<Post>()
        // ...
    }

    override fun onCancelled(databaseError: DatabaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
    }
}
postReference.addValueEventListener(postListener)

Java

ValueEventListener postListener = new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        // Get Post object and use the values to update the UI
        Post post = dataSnapshot.getValue(Post.class);
        // ..
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException());
    }
};
mPostReference.addValueEventListener(postListener);

Listener จะได้รับ DataSnapshot ที่มีข้อมูลในตำแหน่งที่ระบุไว้ในฐานข้อมูลในขณะที่เกิดเหตุการณ์นั้นๆ ขึ้น การเรียกใช้ getValue() ในสแนปชอตจะแสดงการนำเสนอออบเจ็กต์ Java ของข้อมูล หากไม่มีข้อมูลที่ตำแหน่งนั้น การเรียกใช้ getValue() จะแสดงผล null

ในตัวอย่างนี้ ValueEventListener ยังกำหนดเมธอด onCancelled() ที่เรียกใช้หากการอ่านถูกยกเลิก เช่น การอ่านอาจถูกยกเลิกหากไคลเอ็นต์ไม่มีสิทธิ์อ่านจากตำแหน่งฐานข้อมูล Firebase เมธอดนี้จะส่งผ่านออบเจ็กต์ DatabaseError ที่ระบุสาเหตุของความล้มเหลว

อ่านข้อมูลครั้งเดียว

อ่านครั้งเดียวโดยใช้ get()

SDK ออกแบบมาเพื่อจัดการการโต้ตอบกับเซิร์ฟเวอร์ฐานข้อมูลไม่ว่าแอปของคุณจะออนไลน์หรือออฟไลน์

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

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

การใช้ get() โดยไม่จำเป็นอาจเพิ่มการใช้แบนด์วิดท์และทำให้ประสิทธิภาพการทำงานลดลง ซึ่งป้องกันได้โดยใช้ Listener แบบเรียลไทม์ดังที่แสดงด้านบน

Kotlin+KTX

mDatabase.child("users").child(userId).get().addOnSuccessListener {
    Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
    Log.e("firebase", "Error getting data", it)
}

Java

mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
    @Override
    public void onComplete(@NonNull Task<DataSnapshot> task) {
        if (!task.isSuccessful()) {
            Log.e("firebase", "Error getting data", task.getException());
        }
        else {
            Log.d("firebase", String.valueOf(task.getResult().getValue()));
        }
    }
});

อ่านครั้งเดียวโดยใช้ Listener

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

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

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

อัปเดตช่องข้อมูลที่เฉพาะเจาะจง

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

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

Kotlin+KTX

@IgnoreExtraProperties
data class Post(
    var uid: String? = "",
    var author: String? = "",
    var title: String? = "",
    var body: String? = "",
    var starCount: Int = 0,
    var stars: MutableMap<String, Boolean> = HashMap(),
) {

    @Exclude
    fun toMap(): Map<String, Any?> {
        return mapOf(
            "uid" to uid,
            "author" to author,
            "title" to title,
            "body" to body,
            "starCount" to starCount,
            "stars" to stars,
        )
    }
}

Java

@IgnoreExtraProperties
public class Post {

    public String uid;
    public String author;
    public String title;
    public String body;
    public int starCount = 0;
    public Map<String, Boolean> stars = new HashMap<>();

    public Post() {
        // Default constructor required for calls to DataSnapshot.getValue(Post.class)
    }

    public Post(String uid, String author, String title, String body) {
        this.uid = uid;
        this.author = author;
        this.title = title;
        this.body = body;
    }

    @Exclude
    public Map<String, Object> toMap() {
        HashMap<String, Object> result = new HashMap<>();
        result.put("uid", uid);
        result.put("author", author);
        result.put("title", title);
        result.put("body", body);
        result.put("starCount", starCount);
        result.put("stars", stars);

        return result;
    }
}

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

Kotlin+KTX

private fun writeNewPost(userId: String, username: String, title: String, body: String) {
    // Create new post at /user-posts/$userid/$postid and at
    // /posts/$postid simultaneously
    val key = database.child("posts").push().key
    if (key == null) {
        Log.w(TAG, "Couldn't get push key for posts")
        return
    }

    val post = Post(userId, username, title, body)
    val postValues = post.toMap()

    val childUpdates = hashMapOf<String, Any>(
        "/posts/$key" to postValues,
        "/user-posts/$userId/$key" to postValues,
    )

    database.updateChildren(childUpdates)
}

Java

private void writeNewPost(String userId, String username, String title, String body) {
    // Create new post at /user-posts/$userid/$postid and at
    // /posts/$postid simultaneously
    String key = mDatabase.child("posts").push().getKey();
    Post post = new Post(userId, username, title, body);
    Map<String, Object> postValues = post.toMap();

    Map<String, Object> childUpdates = new HashMap<>();
    childUpdates.put("/posts/" + key, postValues);
    childUpdates.put("/user-posts/" + userId + "/" + key, postValues);

    mDatabase.updateChildren(childUpdates);
}

ตัวอย่างนี้ใช้ push() เพื่อสร้างโพสต์ในโหนดที่มีโพสต์สำหรับผู้ใช้ทั้งหมดที่ /posts/$postid และเรียกคีย์ด้วย getKey() พร้อมกัน จากนั้นจะใช้คีย์เพื่อสร้างรายการที่ 2 ในโพสต์ของผู้ใช้ที่ /user-posts/$userid/$postid ได้

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

เพิ่ม Callback ที่เสร็จสมบูรณ์

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

Kotlin+KTX

database.child("users").child(userId).setValue(user)
    .addOnSuccessListener {
        // Write was successful!
        // ...
    }
    .addOnFailureListener {
        // Write failed
        // ...
    }

Java

mDatabase.child("users").child(userId).setValue(user)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void aVoid) {
                // Write was successful!
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Write failed
                // ...
            }
        });

ลบข้อมูล

วิธีที่ง่ายที่สุดในการลบข้อมูลคือการเรียกใช้ removeValue() ตามการอ้างอิงตำแหน่งของข้อมูลนั้น

คุณยังลบได้โดยการระบุ null เป็นค่าสำหรับการดำเนินการเขียนอื่น เช่น setValue() หรือ updateChildren() คุณสามารถใช้เทคนิคนี้กับ updateChildren() เพื่อลบรายการย่อยหลายรายการในการเรียก API เดียวได้

ปลดผู้ฟังออก

ระบบจะนำการติดต่อกลับออกโดยการเรียกใช้เมธอด removeEventListener() ในการอ้างอิงฐานข้อมูล Firebase

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

การเรียกใช้ removeEventListener() ใน Listener หลักไม่ได้เป็นการนำ Listener ที่ลงทะเบียนในโหนดย่อยออกโดยอัตโนมัติ และ removeEventListener() ต้องมีการเรียกในผู้ฟังย่อยทั้งหมดด้วย เพื่อนำ Callback ออก

บันทึกข้อมูลเป็นธุรกรรม

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

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

Kotlin+KTX

private fun onStarClicked(postRef: DatabaseReference) {
    // ...
    postRef.runTransaction(object : Transaction.Handler {
        override fun doTransaction(mutableData: MutableData): Transaction.Result {
            val p = mutableData.getValue(Post::class.java)
                ?: return Transaction.success(mutableData)

            if (p.stars.containsKey(uid)) {
                // Unstar the post and remove self from stars
                p.starCount = p.starCount - 1
                p.stars.remove(uid)
            } else {
                // Star the post and add self to stars
                p.starCount = p.starCount + 1
                p.stars[uid] = true
            }

            // Set value and report transaction success
            mutableData.value = p
            return Transaction.success(mutableData)
        }

        override fun onComplete(
            databaseError: DatabaseError?,
            committed: Boolean,
            currentData: DataSnapshot?,
        ) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError!!)
        }
    })
}

Java

private void onStarClicked(DatabaseReference postRef) {
    postRef.runTransaction(new Transaction.Handler() {
        @NonNull
        @Override
        public Transaction.Result doTransaction(@NonNull MutableData mutableData) {
            Post p = mutableData.getValue(Post.class);
            if (p == null) {
                return Transaction.success(mutableData);
            }

            if (p.stars.containsKey(getUid())) {
                // Unstar the post and remove self from stars
                p.starCount = p.starCount - 1;
                p.stars.remove(getUid());
            } else {
                // Star the post and add self to stars
                p.starCount = p.starCount + 1;
                p.stars.put(getUid(), true);
            }

            // Set value and report transaction success
            mutableData.setValue(p);
            return Transaction.success(mutableData);
        }

        @Override
        public void onComplete(DatabaseError databaseError, boolean committed,
                               DataSnapshot currentData) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError);
        }
    });
}

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

ส่วนเพิ่มจากฝั่งเซิร์ฟเวอร์แบบอะตอม

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

Kotlin+KTX

private fun onStarClicked(uid: String, key: String) {
    val updates: MutableMap<String, Any> = hashMapOf(
        "posts/$key/stars/$uid" to true,
        "posts/$key/starCount" to ServerValue.increment(1),
        "user-posts/$uid/$key/stars/$uid" to true,
        "user-posts/$uid/$key/starCount" to ServerValue.increment(1),
    )
    database.updateChildren(updates)
}

Java

private void onStarClicked(String uid, String key) {
    Map<String, Object> updates = new HashMap<>();
    updates.put("posts/"+key+"/stars/"+uid, true);
    updates.put("posts/"+key+"/starCount", ServerValue.increment(1));
    updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true);
    updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1));
    mDatabase.updateChildren(updates);
}

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

หากต้องการตรวจหาและปฏิเสธความขัดแย้งเฉพาะแอปพลิเคชัน เช่น ผู้ใช้ติดดาวโพสต์ที่ติดดาวไว้ก่อนหน้านี้ คุณควรเขียนกฎความปลอดภัยที่กำหนดเองสำหรับกรณีการใช้งานนั้น

ใช้งานข้อมูลแบบออฟไลน์

หากลูกค้าสูญเสียการเชื่อมต่อเครือข่าย แอปจะทำงานได้อย่างถูกต้องต่อไป

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

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

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

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

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