قراءة وكتابة البيانات على أندرويد

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

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

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

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

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

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

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

تتوفر إرشادات تفصيلية تتضمن قاعدة بيانات Realtime ووظائف السحابة . يجب عليك أيضًا إلقاء نظرة على مقدمة Local Emulator Suite .

الحصول على مرجع قاعدة البيانات

لقراءة أو كتابة البيانات من قاعدة البيانات، تحتاج إلى مثيل 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() لحذف عدة توابع في استدعاء واحد لواجهة برمجة التطبيقات (API).

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

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

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

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

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

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

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

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

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

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

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

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