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

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

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

(اختياري) إنشاء النماذج الأولية واختبارها باستخدام Firebase Local Emulator Suite

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

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

يتطلّب استخدام محاكي Realtime Database بضع خطوات فقط:

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

يتوفّر دليل تفصيلي يتضمّن Realtime Database وCloud Functions. ننصحك أيضًا بالاطّلاع على مقدمة حول Local Emulator Suite.

الحصول على DatabaseReference

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

Kotlin

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

@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

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

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

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

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

@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

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

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

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

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 هذه البيانات مع خوادم قواعد البيانات البعيدة ومع العملاء الآخرين على أساس "بذل قصارى الجهد".

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

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

سنتحدّث أكثر عن السلوك بلا إنترنت في مقالة مزيد من المعلومات حول الإمكانات على الإنترنت وبلا إنترنت.

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