قراءة البيانات وكتابتها على 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 الأساسي كالمعتاد، أو باستخدام واجهة برمجة التطبيقات Realtime Database REST API

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

الحصول على 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() في أي مستمعين فرعيين لإزالة طلب الاستدعاء.

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

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

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

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

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

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

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

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