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

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