يتناول هذا المستند أساسيات قراءة بيانات 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 كالمعتاد، أو باستخدام واجهة برمجة التطبيقات REST الخاصة بمنصة 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 هذه البيانات مع خوادم قاعدة البيانات البعيدة ومع البرامج الأخرى على أساس "بذل قصارى الجهد".
نتيجةً لذلك، تؤدي جميع عمليات الكتابة إلى قاعدة البيانات إلى تشغيل الأحداث المحلية على الفور، قبل أي تفاعل مع الخادم. وهذا يعني أنّ تطبيقك سيظلّ يستجيب بغض النظر عن وقت استجابة الشبكة أو الاتصال بها.
وبعد إعادة إنشاء الاتصال، يتلقّى تطبيقك مجموعة الأحداث المناسبة لكي تتم مزامنة العميل مع حالة الخادم الحالية، بدون الحاجة إلى كتابة أي رمز مخصّص.
سنتحدّث أكثر عن السلوك غير الإلكتروني في مقالة مزيد من المعلومات عن الإمكانات على الإنترنت وخارجه.