العمل باستخدام قوائم البيانات على Android

يتناول هذا المستند العمل باستخدام قوائم البيانات في Firebase. لمعرفة أساسيات قراءة بيانات Firebase وكتابتها، يمكنك الاطّلاع على قراءة البيانات وكتابتها على Android.

الحصول على DatabaseReference

لقراءة البيانات وكتابتها من قاعدة البيانات، تحتاج إلى مثيل لـ DatabaseReference:

Kotlin+KTX

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

Java

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

قراءة القوائم وكتابتها

إلحاق بقائمة من البيانات

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

ويمكنك استخدام الإشارة إلى البيانات الجديدة التي تعرضها طريقة push() للحصول على قيمة المفتاح الذي تم إنشاؤه تلقائيًا للطفل أو ضبط البيانات الخاصة بالطفل. يؤدي استدعاء getKey() في مرجع push() إلى عرض قيمة المفتاح الذي يتم إنشاؤه تلقائيًا.

يمكنك استخدام هذه المفاتيح التي تم إنشاؤها تلقائيًا لتبسيط هيكلة بياناتك. لمزيد من المعلومات، يمكنك الاطّلاع على مثال توزيع البيانات.

الاستماع إلى الأحداث الفرعية

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

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

للاستماع إلى الأحداث الفرعية على DatabaseReference، يمكنك إرفاق ChildEventListener:

المستمع معاودة الاتصال بالحدث معدّل الاستخدام
ChildEventListener onChildAdded() استرداد قوائم العناصر أو الاستماع إلى الإضافات إلى قائمة من العناصر. يتم تشغيل معاودة الاتصال هذه مرة واحدة لكل حساب ثانوي حالي ثم مرة أخرى في كل مرة تتم فيها إضافة عنصر ثانوي جديد إلى المسار المحدد. وتحتوي قيمة DataSnapshot التي يتم إرسالها إلى المستمع على بيانات الطفل الجديد.
onChildChanged() رصد التغييرات على العناصر في القائمة يتم تنشيط هذا الحدث في أي وقت يتم فيه تعديل عقدة فرعية، بما في ذلك أي تعديلات على العناصر التابعة للعقدة الفرعية. تحتوي السمة DataSnapshot التي يتم تمريرها إلى أداة معالجة الحدث على البيانات المعدّلة الخاصة بالطفل.
onChildRemoved() رصد العناصر التي تتم إزالتها من القائمة تحتوي السمة DataSnapshot التي يتم تمريرها إلى معاودة الاتصال بالحدث على بيانات الطفل الذي تمت إزالته.
onChildMoved() الاستماع إلى التغييرات التي تطرأ على ترتيب العناصر في قائمة مع ترتيب يتم تشغيل هذا الحدث عند تشغيل معاودة الاتصال onChildChanged() من خلال تعديل يؤدي إلى إعادة ترتيب الوحدة الفرعية. ويتم استخدام هذه القيمة مع البيانات المطلوبة باستخدام orderByChild أو orderByValue.

على سبيل المثال، قد يستخدم تطبيق التدوين الاجتماعي هذه الأساليب معًا لمراقبة النشاط في تعليقات المشاركة، كما هو موضح أدناه:

Kotlin+KTX

val childEventListener = object : ChildEventListener {
    override fun onChildAdded(dataSnapshot: DataSnapshot, previousChildName: String?) {
        Log.d(TAG, "onChildAdded:" + dataSnapshot.key!!)

        // A new comment has been added, add it to the displayed list
        val comment = dataSnapshot.getValue<Comment>()

        // ...
    }

    override fun onChildChanged(dataSnapshot: DataSnapshot, previousChildName: String?) {
        Log.d(TAG, "onChildChanged: ${dataSnapshot.key}")

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so displayed the changed comment.
        val newComment = dataSnapshot.getValue<Comment>()
        val commentKey = dataSnapshot.key

        // ...
    }

    override fun onChildRemoved(dataSnapshot: DataSnapshot) {
        Log.d(TAG, "onChildRemoved:" + dataSnapshot.key!!)

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so remove it.
        val commentKey = dataSnapshot.key

        // ...
    }

    override fun onChildMoved(dataSnapshot: DataSnapshot, previousChildName: String?) {
        Log.d(TAG, "onChildMoved:" + dataSnapshot.key!!)

        // A comment has changed position, use the key to determine if we are
        // displaying this comment and if so move it.
        val movedComment = dataSnapshot.getValue<Comment>()
        val commentKey = dataSnapshot.key

        // ...
    }

    override fun onCancelled(databaseError: DatabaseError) {
        Log.w(TAG, "postComments:onCancelled", databaseError.toException())
        Toast.makeText(
            context,
            "Failed to load comments.",
            Toast.LENGTH_SHORT,
        ).show()
    }
}
databaseReference.addChildEventListener(childEventListener)

Java

ChildEventListener childEventListener = new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
        Log.d(TAG, "onChildAdded:" + dataSnapshot.getKey());

        // A new comment has been added, add it to the displayed list
        Comment comment = dataSnapshot.getValue(Comment.class);

        // ...
    }

    @Override
    public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
        Log.d(TAG, "onChildChanged:" + dataSnapshot.getKey());

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so displayed the changed comment.
        Comment newComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();

        // ...
    }

    @Override
    public void onChildRemoved(DataSnapshot dataSnapshot) {
        Log.d(TAG, "onChildRemoved:" + dataSnapshot.getKey());

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so remove it.
        String commentKey = dataSnapshot.getKey();

        // ...
    }

    @Override
    public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
        Log.d(TAG, "onChildMoved:" + dataSnapshot.getKey());

        // A comment has changed position, use the key to determine if we are
        // displaying this comment and if so move it.
        Comment movedComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();

        // ...
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        Log.w(TAG, "postComments:onCancelled", databaseError.toException());
        Toast.makeText(mContext, "Failed to load comments.",
                Toast.LENGTH_SHORT).show();
    }
};
databaseReference.addChildEventListener(childEventListener);

الاطّلاع على الأحداث القيّمة

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

سيؤدي إرفاق ValueEventListener بقائمة من البيانات إلى عرض قائمة البيانات الكاملة على شكل DataSnapshot واحد، ويمكنك تكرارها للوصول إلى العناصر الثانوية الفردية.

حتى عندما يكون هناك تطابق واحد فقط لطلب البحث، تظل اللقطة قائمة؛ بل تحتوي فقط على عنصر واحد. للوصول إلى العنصر، تحتاج إلى التكرار حول النتيجة:

Kotlin+KTX

// My top posts by number of stars
myTopPostsQuery.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        for (postSnapshot in dataSnapshot.children) {
            // TODO: handle the post
        }
    }

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

Java

// My top posts by number of stars
myTopPostsQuery.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
        for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) {
            // TODO: handle the post
        }
    }

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

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

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

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

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

لا يؤدي استدعاء removeEventListener() على مستمع رئيسي إلى إزالة المستمعين المسجلين تلقائيًا في العُقد الفرعية الخاصة به، ويجب أيضًا استدعاء removeEventListener() على أي مستمع طفل لإزالة رد الاتصال.

فرز البيانات وتصفيتها

يمكنك استخدام الفئة Query لقاعدة بيانات "الوقت الفعلي" لاسترداد البيانات مرتّبة حسب المفتاح أو القيمة أو القيمة التابعة لجهة فرعية. يمكنك أيضًا تصفية النتيجة التي تم فرزها إلى عدد محدد من النتائج أو نطاق من المفاتيح أو القيم.

فرز البيانات

لاسترداد البيانات التي تم فرزها، ابدأ بتحديد إحدى طرق الترتيب حسب لتحديد كيفية ترتيب النتائج:

الطريقة الاستخدام
orderByChild() يمكنك ترتيب النتائج حسب قيمة مفتاح فرعي محدّد أو مسار فرعي متداخل.
orderByKey() يمكنك ترتيب النتائج حسب المفاتيح الثانوية.
orderByValue() ترتيب النتائج حسب القيم الثانوية

يمكنك استخدام طريقة واحدة فقط لكل طلب على حدة في كل مرة. يؤدي استدعاء طريقة الترتيب حسب عدة مرات في نفس الاستعلام إلى حدوث خطأ.

يوضح المثال التالي كيف يمكنك استرداد قائمة بأهم مشاركات المستخدم مرتبةً حسب عدد النجوم:

Kotlin+KTX

// My top posts by number of stars
val myUserId = uid
val myTopPostsQuery = databaseReference.child("user-posts").child(myUserId)
    .orderByChild("starCount")

myTopPostsQuery.addChildEventListener(object : ChildEventListener {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
})

Java

// My top posts by number of stars
String myUserId = getUid();
Query myTopPostsQuery = databaseReference.child("user-posts").child(myUserId)
        .orderByChild("starCount");
myTopPostsQuery.addChildEventListener(new ChildEventListener() {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
});

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

يحدد استدعاء الإجراء orderByChild() المفتاح الفرعي لترتيب النتائج حسبه. في هذه الحالة، يتم ترتيب المشاركات حسب قيمة عنصر "starCount" الفرعي المعني. يمكن أيضًا ترتيب الاستعلامات حسب العناصر الثانوية المتداخلة، في حال كان لديك بيانات تبدو كالتالي:

"posts": {
  "ts-functions": {
    "metrics": {
      "views" : 1200000,
      "likes" : 251000,
      "shares": 1200,
    },
    "title" : "Why you should use TypeScript for writing Cloud Functions",
    "author": "Doug",
  },
  "android-arch-3": {
    "metrics": {
      "views" : 900000,
      "likes" : 117000,
      "shares": 144,
    },
    "title" : "Using Android Architecture Components with Firebase Realtime Database (Part 3)",
    "author": "Doug",
  }
},

في هذا المثال، يمكننا ترتيب عناصر القائمة حسب القيم المتداخلة ضمن مفتاح metrics من خلال تحديد المسار النسبي للعنصر الفرعي المدمج في طلب orderByChild().

Kotlin+KTX

// Most viewed posts
val myMostViewedPostsQuery = databaseReference.child("posts")
    .orderByChild("metrics/views")
myMostViewedPostsQuery.addChildEventListener(object : ChildEventListener {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
})

Java

// Most viewed posts
Query myMostViewedPostsQuery = databaseReference.child("posts")
        .orderByChild("metrics/views");
myMostViewedPostsQuery.addChildEventListener(new ChildEventListener() {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
});

لمزيد من المعلومات حول كيفية ترتيب أنواع البيانات الأخرى، راجِع كيفية ترتيب بيانات طلبات البحث.

فلترة البيانات

لتصفية البيانات، يمكنك دمج أي من طرق الحد أو النطاق مع طريقة الترتيب حسب عند إنشاء استعلام.

الطريقة الاستخدام
limitToFirst() تحدد هذه السمة الحد الأقصى لعدد العناصر المطلوب عرضها من بداية قائمة النتائج المرتبة.
limitToLast() تحدد هذه السمة الحد الأقصى لعدد العناصر المطلوب عرضها من نهاية قائمة النتائج المرتبة.
startAt() يمكنك عرض عناصر أكبر من أو تساوي المفتاح أو القيمة المحدّدة بناءً على طريقة الترتيب المحدّدة.
startAfter() يمكنك إرجاع عناصر أكبر من المفتاح أو القيمة المحدّدة بناءً على طريقة الترتيب المحدّدة.
endAt() يمكنك عرض عناصر أقل من أو تساوي المفتاح أو القيمة المحدّدة بناءً على طريقة الترتيب المحدّدة.
endBefore() يمكنك إرجاع عناصر أقل من المفتاح أو القيمة المحدّدة بناءً على طريقة الترتيب المحدّدة.
equalTo() يمكنك عرض عناصر مساوية للمفتاح أو القيمة المحدّدة بناءً على طريقة الترتيب المحدّدة.

على عكس طرق الترتيب حسب، يمكنك الجمع بين دوال متعددة للحدود أو النطاقات. على سبيل المثال، يمكنك الجمع بين الطريقتَين startAt() وendAt() لحصر النتائج على نطاق محدّد من القيم.

حتى عندما يكون هناك تطابق واحد فقط للاستعلام، فإن اللقطة لا تزال قائمة؛ بل تحتوي فقط على عنصر واحد. للوصول إلى العنصر، تحتاج إلى التكرار فوق النتيجة:

Kotlin+KTX

// My top posts by number of stars
myTopPostsQuery.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        for (postSnapshot in dataSnapshot.children) {
            // TODO: handle the post
        }
    }

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

Java

// My top posts by number of stars
myTopPostsQuery.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
        for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) {
            // TODO: handle the post
        }
    }

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

تحديد عدد النتائج

يمكنك استخدام الطريقتَين limitToFirst() وlimitToLast() لضبط حدّ أقصى لعدد العناصر الثانوية المطلوب مزامنتها عند معاودة اتصال معيّنة. على سبيل المثال، إذا استخدمت limitToFirst() لضبط حدّ أقصى يبلغ 100، لن تتلقّى في البداية سوى 100 onChildAdded() استدعاء. إذا كان لديك أقل من 100 عنصر مخزَّن في قاعدة بيانات Firebase، يتم تنشيط استدعاء onChildAdded() لكل عنصر.

ومع تغيُّر العناصر، ستتلقّى onChildAdded() طلبات معاودة الاتصال للعناصر التي تُدخل طلب البحث وonChildRemoved() عمليات استدعاء للعناصر التي يتم الانسحاب منها حتى يظل العدد الإجمالي هو 100.

يوضح المثال التالي كيف يحدد تطبيق تدوين البيانات استعلامًا لاسترداد قائمة بأحدث 100 مشاركة نشرها جميع المستخدمين:

Kotlin+KTX

// Last 100 posts, these are automatically the 100 most recent
// due to sorting by push() keys.
databaseReference.child("posts").limitToFirst(100)

Java

// Last 100 posts, these are automatically the 100 most recent
// due to sorting by push() keys
Query recentPostsQuery = databaseReference.child("posts")
        .limitToFirst(100);

يحدد هذا المثال طلب بحث فقط، ولمزامنة البيانات التي يجب أن يتوفر لها أداة استماع مرفقة.

الفلترة حسب المفتاح أو القيمة

يمكنك استخدام startAt() وstartAfter() وendAt() وendBefore() وequalTo() لاختيار نقاط بداية ونهاية وتكافؤ عشوائية لطلبات البحث. يمكن أن يكون هذا مفيدًا لتقسيم البيانات إلى صفحات أو البحث عن عناصر بها أطفال لها قيمة محددة.

كيف يتم ترتيب بيانات طلبات البحث

يشرح هذا القسم كيفية ترتيب البيانات حسب كل طريقة من الطرق بالترتيب في الفئة Query.

orderByChild

عند استخدام orderByChild()، يتم ترتيب البيانات التي تحتوي على المفتاح الفرعي المحدّد على النحو التالي:

  1. تأتي العناصر الثانوية التي لها قيمة null للمفتاح الفرعي المحدّد في المقام الأول.
  2. تأتي العناصر الثانوية بقيمة false للمفتاح الفرعي المحدّد بعد ذلك. إذا كانت القيمة false لعدة عناصر ثانوية، يتم ترتيبها معجميًا حسب المفتاح.
  3. تأتي العناصر الثانوية بقيمة true للمفتاح الفرعي المحدّد بعد ذلك. إذا كانت القيمة true لعدة عناصر فرعية، يتم ترتيبها قاموسًا حسب المفتاح.
  4. تأتي الأطفال ذوي القيمة الرقمية بعد ذلك، مرتبة بترتيب تصاعدي. إذا كانت هناك عدة عناصر ثانوية لها القيمة الرقمية نفسها للعقدة الفرعية المحدّدة، يتم ترتيبها حسب المفتاح.
  5. تأتي السلاسل بعد الأرقام ويتم ترتيبها بشكل قاموس بترتيب تصاعدي. إذا كانت هناك عدة عناصر ثانوية لها نفس القيمة للعقدة الفرعية المحددة، يتم ترتيبها حسب العنوان.
  6. تأتي الكائنات في النهاية ويتم فرزها لغويًا حسب المفتاح بترتيب تصاعدي.

orderByKey

عند استخدام orderByKey() لترتيب بياناتك، يتم عرض البيانات بترتيب تصاعدي حسب المفتاح.

  1. تأتي العناصر الثانوية التي تتضمن مفتاحًا يمكن تحليله كعدد صحيح 32 بت أولاً، ويتم ترتيبها تصاعديًا.
  2. يأتي العناصر الثانوية التي لها قيمة سلسلة كمفتاحها بعد ذلك، ويتم فرزها ترتيبًا تصاعديًا.

orderByValue

عند استخدام orderByValue()، يتم ترتيب العناصر الثانوية حسب قيمتها. معايير الترتيب هي نفسها في orderByChild()، باستثناء قيمة العقدة المستخدَمة بدلاً من قيمة مفتاح فرعي محدّد.

الخطوات اللاحقة