يغطّي هذا المستند أساسيات قراءة بيانات Firebase وكتابتها.
تتم كتابة بيانات Firebase في مرجع FirebaseDatabase ويتم استردادها من خلال إرفاق مستمع غير متزامن بالمرجع. يتم تفعيل المستمع مرة واحدة للحالة الأولية للبيانات ومرة أخرى في أي وقت تتغيّر فيه البيانات.
(اختياري) إنشاء نموذج أولي واختباره باستخدام Firebase Local Emulator Suite
قبل التحدّث عن كيفية قراءة تطبيقك من Realtime Database والكتابة فيه، لنقدّم مجموعة من الأدوات التي يمكنك استخدامها لإنشاء نموذج أولي لوظائف Realtime Database واختبارها: Firebase Local Emulator Suite. إذا كنت تجرب نماذج بيانات مختلفة أو تحسّن قواعد الأمان أو تعمل على إيجاد الطريقة الأكثر فعالية من حيث التكلفة للتفاعل مع الخادم الخلفي، يمكن أن يكون العمل محليًا بدون نشر الخدمات المباشرة فكرة رائعة.
يُعدّ محاكي Realtime Database جزءًا من Local Emulator Suite، ما يتيح لتطبيقك التفاعل مع محتوى قاعدة البيانات المحاكية وإعداداتها، بالإضافة إلى موارد مشروعك المحاكية (الدوال وقواعد البيانات الأخرى وقواعد الأمان) بشكل اختياري.
لا يتطلّب استخدام محاكي Realtime Database سوى بضع خطوات:
- إضافة سطر من الرمز إلى إعدادات الاختبار في تطبيقك للاتصال بالمحاكي
- تشغيل
firebase emulators:startمن جذر دليل مشروع على جهاز المستخدم المحلي - إجراء طلبات من الرمز الأولي لتطبيقك باستخدام حزمة تطوير برامج (SDK) لمنصة Realtime Database كالمعتاد، أو باستخدام Realtime Database REST API.
يتوفّر شرح تفصيلي يتضمّن و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 المتاحة على النحو التالي:
StringLongDoubleBooleanMap<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()
method لإضافة 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 هذه البيانات مع خوادم قاعدة البيانات البعيدة ومع العملاء الآخرين على أساس "بذل أفضل جهد".
نتيجةً لذلك، تؤدي جميع عمليات الكتابة في قاعدة البيانات إلى تفعيل الأحداث المحلية على الفور، قبل أي تفاعل مع الخادم. وهذا يعني أنّ تطبيقك يظل سريع الاستجابة بغض النظر عن وقت استجابة الشبكة أو الاتصال بها.
بمجرد استعادة الاتصال، يتلقّى تطبيقك المجموعة المناسبة من الأحداث حتى تتم مزامنة العميل مع حالة الخادم الحالية، بدون الحاجة إلى كتابة أي رمز مخصّص.
سنتحدّث أكثر عن السلوك بلا اتصال بالإنترنت في مزيد من المعلومات عن الإمكانات المتصلة بالإنترنت وغير المتصلة به.