قراءة البيانات وكتابتها على Android

يتناول هذا المستند أساسيات قراءة بيانات Firebase وكتابتها.

تتم كتابة بيانات Firebase في مرجع FirebaseDatabase ويتم استردادها بحلول إرفاق مستمع غير متزامن بالمرجع. يتم تشغيل المستمع مرة للحالة الأولية للبيانات ومرة أخرى في أي وقت تتغير البيانات.

(اختياري) إنشاء نموذج أولي واختباره باستخدام "حزمة أدوات المحاكاة المحلية من Firebase"

قبل الحديث عن كيفية قراءة تطبيقك لقاعدة بيانات Realtime، والكتابة إليها، نقدم مجموعة من الأدوات التي يمكنك استخدامها لإنشاء نموذج أولي واختبار قاعدة البيانات في الوقت الفعلي الوظيفة: مجموعة أدوات المحاكاة المحلية لمنصة Firebase. إذا كنت تجرب بيانات مختلفة أو تحسين قواعد الأمان أو العمل على العثور على فعّالة من حيث التكلفة للتفاعل مع الواجهة الخلفية، والقدرة على العمل محليًا بدون نشر خدمات مباشرة.

ويُعد محاكي قاعدة البيانات في الوقت الفعلي جزءًا من مجموعة أدوات المحاكاة المحلية، التي للتطبيق التفاعل مع محتوى قاعدة البيانات التي تمّت محاكاتها وإعداداتها، بالإضافة إلى موارد المشروع التي تمت محاكاتها (الدوال وقواعد البيانات الأخرى وقواعد الأمان).

يتضمن استخدام محاكي قاعدة البيانات في الوقت الفعلي بضع خطوات فقط:

  1. إضافة سطر من الرمز إلى إعدادات اختبار تطبيقك للاتصال بالمحاكي.
  2. من جذر دليل المشروع المحلي، مع تشغيل firebase emulators:start.
  3. إجراء الاتصالات من رمز النموذج الأولي لتطبيقك باستخدام النظام الأساسي لقاعدة البيانات في الوقت الفعلي SDK كالمعتاد، أو تستخدم Realtime Database REST API.

تتوفّر جولة تفصيلية تشمل قاعدة بيانات "الوقت الفعلي" و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 مخصّص، إذا كانت الفئة التي تحدِّده تتضمَّن فئة الدالة الإنشائية التي لا تأخذ أي وسيطات ولديها استدعاءات عامة للخصائص أن يتم تعيينه.

إذا كنت تستخدم كائن 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);

قراءة البيانات

قراءة البيانات باستخدام أدوات معالجة الأحداث بشكل دائم

لقراءة البيانات في أحد المسارات والاطّلاع على التغييرات، يمكنك استخدام addValueEventListener(). لإضافة ValueEventListener إلى DatabaseReference.

المستمع معاودة الاتصال بالحدث معدّل الاستخدام
ValueEventListener onDataChange() قراءة التغييرات التي تتم على محتوى المسار بالكامل والاستماع إليها

ويمكنك استخدام الطريقة onDataChange() لقراءة نبذة ثابتة عن في مسار معين، حيث كانت موجودة وقت الحدث. هذه الطريقة مرة واحدة عند توصيل المستمع ومرة أخرى في كل مرة يتم فيها نقل البيانات، بما في ذلك الأطفال والتغييرات. يحصل معاودة الاتصال بالحدث على نبذة تحتوي على جميع البيانات في هذا الموقع، بما في ذلك البيانات الفرعية. إذا لم تكن هناك بيانات، ستعرض اللقطة 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);

يتلقّى المستمع DataSnapshot الذي يحتوي على البيانات على النطاق الموقع في قاعدة البيانات وقت الحدث. جارٍ الاتصال بالرقم getValue() على لقطة شاشة تمثيل كائن Java للبيانات. في حال عدم توفّر بيانات في الموقع، فإن الاتصال بـ getValue() يعرض null.

في هذا المثال، تعرّف ValueEventListener أيضًا طريقة onCancelled() التي في حال إلغاء القراءة. على سبيل المثال، يمكن إلغاء إحدى عمليات القراءة إذا كانت لا يملك العميل إذنًا بالقراءة من موقع قاعدة بيانات Firebase. هذا النمط تمرير عنصر DatabaseError للإشارة إلى سبب حدوث العطل.

قراءة البيانات مرة واحدة

القراءة مرة واحدة باستخدام get()

تم تصميم حزمة SDK لإدارة التفاعلات مع خوادم قاعدة البيانات سواء كانت تطبيقك متصلاً بالإنترنت أو غير متصل بالإنترنت.

بشكل عام، عليك استخدام أساليب ValueEventListener الموضّحة أعلاه. قراءة البيانات لتلقي إشعارات بالتحديثات التي تطرأ على البيانات من الخلفية. تشير رسالة الأشكال البيانية تقلل تقنيات المستمع من الاستخدام والفوترة، ويتم تحسينها لمنح المستخدمين أفضل تجربة عند استخدام الإنترنت وبلا اتصال بالإنترنت.

إذا كنت بحاجة إلى البيانات مرة واحدة فقط، يمكنك استخدام get() للحصول على نبذة بيانات من قاعدة البيانات. في حال تعذُّر إرجاع الخادم على get() لأي سبب فسيتم فحص العميل لذاكرة التخزين المؤقت على الجهاز وعرض رسالة خطأ إذا لم يتم العثور على القيمة حتى الآن.

فالاستخدام غير الضروري لـ get() قد يزيد من استخدام معدل نقل البيانات ويؤدي إلى فقدان الأداء، والذي يمكن منعه باستخدام أداة معالجة الأحداث في الوقت الفعلي كما هو موضح أعلاه.

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()));
        }
    }
});

القراءة مرة واحدة باستخدام مستمع

في بعض الحالات، قد تحتاج إلى عرض القيمة من ذاكرة التخزين المؤقت المحلية على الفور، بدلاً من البحث عن أي قيمة محدَّثة على الخادم. في تلك في الحالات، يمكنك استخدام 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() ويمكن بعد ذلك استخدام المفتاح لإنشاء إدخال ثانٍ في ملف المشاركات في /user-posts/$userid/$postid.

باستخدام هذه المسارات، يمكنك إجراء تحديثات متزامنة للعديد من المواقع في شجرة JSON مع استدعاء واحد إلى updateChildren()، مثل كيف يظهر هذا المثال تنشئ المشاركة الجديدة في كلا الموقعين. التحديثات المتزامنة التي تم إجراؤها بهذه الطريقة بسيطة: إما أن تنجح جميع التحديثات أو أن جميع التحديثات تفشل.

إضافة معاودة اتصال مكتملة

إذا كنت تريد معرفة وقت الالتزام ببياناتك، يمكنك إضافة مستمع إكمال. يجب تحديد قيمة اختيارية لكل من setValue() وupdateChildren(). مستمِع لإكمال العملية يتم استدعاءه عند إتمام عملية الكتابة بنجاح ملتزم بقاعدة البيانات. إذا لم تكن المكالمة ناجحة، يعني ذلك أنّ المستمع تمرير عنصر خطأ يشير إلى سبب حدوث الفشل.

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() لحذف عدة عناصر فرعية في طلب بيانات واحد من واجهة برمجة التطبيقات.

فصل المستمعين

تتم إزالة عمليات معاودة الاتصال من خلال استدعاء طريقة removeEventListener() على مرجع قاعدة بيانات Firebase.

إذا تمت إضافة مستمِع عدة مرات إلى موقع بيانات معيّن، استدعائها عدة مرات لكل حدث، ويجب فصله بنفس عدد عدة مرات لإزالته تمامًا.

لا يؤدي الاتصال بـ "removeEventListener()" على مستمِع أحد الوالدَين إزالة المستمعين المسجَّلين في العُقد الفرعية تلقائيًا يجب أيضًا استدعاء removeEventListener() على أي أدوات استماع تابعة للأطفال لإزالة رد الاتصال.

حفظ البيانات كمعاملات

عند التعامل مع بيانات قد تكون تالفة بسبب مثل التعديلات التزايدية، يمكنك استخدام عملية المعاملات. وتمنح هذه العملية وسيطتين: دالة تحديث ودالة اختيارية إكمال الاتصال. تأخذ دالة التحديث الحالة الحالية للبيانات وسيطة وتُرجع الحالة الجديدة المطلوبة التي تريد كتابتها. في حال حذف يكتب عميل آخر إلى الموقع قبل أن يتم إنشاء القيمة الجديدة بنجاح مكتوبة، يتم استدعاء دالة التحديث مرة أخرى بالقيمة الحالية الجديدة، تتم إعادة محاولة الكتابة.

على سبيل المثال، في تطبيق التدوين الاجتماعي مثلاً، يمكنك السماح للمستخدمين تمييز المشاركات بنجمة أو إلغاء تمييزها وتتبُّع عدد النجوم التي حصلت عليها إحدى المشاركات على النحو التالي:

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);
        }
    });
}

يؤدي استخدام معاملة ما إلى منع عدد النجوم من أن يكون غير صحيح إذا كانت عدة عناصر يميّز المستخدمون المشاركة نفسها بنجمة في الوقت نفسه، أو تكون لدى العميل بيانات قديمة. إذا كانت يتم رفض المعاملة، يقوم الخادم بإرجاع القيمة الحالية إلى العميل، الذي يجري المعاملة مرّة أخرى بالقيمة المعدّلة يتكرر هذا حتى تم قبول المعاملة أو أجريت محاولات كثيرة جدًا.

الإضافات البسيطة من جهة الخادم

في حالة الاستخدام المذكورة أعلاه، نكتب قيمتين في قاعدة البيانات: معرف المستخدم الذي يميّز المشاركة أو يلغي تمييزها، وعدد النجوم المتزايد إذا نعرف مسبقًا أن المستخدم يميّز المشاركة بنجمة، ويمكننا استخدام جزء بسيط عملية بدلاً من معاملة.

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 بنسخته الداخلية الخاصة من أي بيانات يتم استخدام المستمعين حولها أو التي يتم وضع علامة عليها للاحتفاظ بها المزامنة مع الخادم. عند قراءة أو كتابة البيانات، فإن هذا الإصدار المحلي من استخدام البيانات أولاً. بعد ذلك، يُزامِن برنامج Firebase هذه البيانات مع وخوادم قواعد البيانات البعيدة ومع العملاء الآخرين "بأفضل جهد" بشكل أساسي.

ونتيجةً لذلك، تؤدي جميع عمليات الكتابة في قاعدة البيانات إلى تشغيل الأحداث المحلية على الفور، قبل أي تفاعل مع الخادم. يعني ذلك أنّ تطبيقك يظل متجاوبًا مع مختلف الأجهزة. بغض النظر عن وقت استجابة الشبكة أو الاتصال.

وبمجرد إعادة الاتصال، يتلقى تطبيقك المجموعة المناسبة من بحيث يتزامن البرنامج مع حالة الخادم الحالية، دون الحاجة إلى وكتابة أي رمز مخصص.

سنتحدث أكثر عن السلوك خارج الإنترنت في مزيد من المعلومات حول الإمكانات على الإنترنت وبلا إنترنت

الخطوات التالية