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

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

الحصول على مرجع DatabaseReference

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

Kotlin

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

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

// 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() على أي متتبِّعين ثانويين لإزالة معاودة الاتصال.

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

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

فرز البيانات

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

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

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

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

Kotlin

// 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

// 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

// 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

// 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()، باستثناء أنّه يتم استخدام قيمة العقدة بدلاً من قيمة مفتاح ثانوي محدّد.

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