يتناول هذا المستند الطرق الأربع لكتابة البيانات في Firebase Realtime Database: الضبط والتعديل والدفع ودعم المعاملات.
طُرق توفير البيانات
| set | كتابة البيانات أو استبدالها في مسار محدّد، مثل messages/users/<username> |
| تعديل | تعديل بعض المفاتيح لمسار محدّد بدون استبدال جميع البيانات |
| دفع | إضافة إلى قائمة بيانات في قاعدة البيانات في كل مرة تدفع فيها عقدة جديدة إلى قائمة، تنشئ قاعدة البيانات مفتاحًا فريدًا، مثل messages/users/<unique-user-id>/<username> |
| عملية | استخدام المعاملات عند التعامل مع بيانات معقّدة قد تتلف بسبب التعديلات المتزامنة |
حفظ البيانات
عملية كتابة قاعدة البيانات الأساسية هي مجموعة تحفظ البيانات الجديدة في مرجع قاعدة البيانات المحدّد، لتحلّ محلّ أي بيانات حالية في هذا المسار. لفهم طريقة عمل set، سننشئ تطبيقًا بسيطًا للتدوين. يتم تخزين بيانات تطبيقك في مرجع قاعدة البيانات هذا:
Java
final FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("server/saving-data/fireblog");
Node.js
// Import Admin SDK const { getDatabase } = require('firebase-admin/database'); // Get a database reference to our blog const db = getDatabase(); const ref = db.ref('server/saving-data/fireblog');
Python
# Import database module. from firebase_admin import db # Get a database reference to our blog. ref = db.reference('server/saving-data/fireblog')
متابعة
// Create a database client from App. client, err := app.Database(ctx) if err != nil { log.Fatalln("Error initializing database client:", err) } // Get a database reference to our blog. ref := client.NewRef("server/saving-data/fireblog")
لنبدأ بحفظ بعض بيانات المستخدمين. سنخزّن كل مستخدم باسم مستخدم فريد، وسنخزّن أيضًا اسمه الكامل وتاريخ ميلاده. بما أنّ كل مستخدم سيكون لديه اسم مستخدم فريد، من المنطقي استخدام طريقة set هنا بدلاً من طريقة push لأنّك تملك المفتاح ولا تحتاج إلى إنشاء مفتاح جديد.
أولاً، أنشئ مرجعًا لقاعدة البيانات لبيانات المستخدمين. بعد ذلك، استخدِم set() / setValue() لحفظ عنصر مستخدم في قاعدة البيانات يتضمّن اسم المستخدم والاسم الكامل وتاريخ الميلاد. يمكنك ضبط سلسلة أو رقم أو قيمة منطقية أو null أو مصفوفة أو أي عنصر JSON. سيؤدي تمرير null إلى إزالة البيانات في الموقع المحدّد. في هذه الحالة، ستمرّر كائنًا:
Java
public static class User { public String date_of_birth; public String full_name; public String nickname; public User(String dateOfBirth, String fullName) { // ... } public User(String dateOfBirth, String fullName, String nickname) { // ... } } DatabaseReference usersRef = ref.child("users&qu<ot;); MapSt>ring, User users = n<>ew HashMap(); users.put("alanisawesome", new User("June 23, 1912", "Alan Turing")); users.put("gracehop", new User("December 9, 1906", "Grace Hopper")); usersRef.setValueAsync(users);
Node.js
const usersRef = ref.child('users'); usersRef.set({ alanisawesome: { date_of_birth: 'June 23, 1912', full_name: 'Alan Turing' }, gracehop: { date_of_birth: 'December 9, 1906', full_name: 'Grace Hopper' } });
Python
users_ref = ref.child('users') users_ref.set({ 'alanisawesome': { 'date_of_birth': 'June 23, 1912', 'full_name': 'Alan Turing' }, 'gracehop': { 'date_of_birth': 'December 9, 1906', 'full_name': 'Grace Hopper' } })
متابعة
// User is a json-serializable type. type User struct { DateOfBirth string `json:"date_of_birth,omitempty"` FullName string `json:"full_name,omitempty"` Nickname string `json:"nickname,omitempty"` } usersRef := ref.Child("users") err := usersRef.Set(ctx, map[string]*User{ "alanisawesome": { DateOfBirth: "June 23, 1912", FullName: "Alan Turing", }, "gracehop": { DateOfBirth: "December 9, 1906", FullName: "Grace Hopper", }, }) if err != nil { log.Fatalln("Error setting value:", err) }
عند حفظ عنصر JSON في قاعدة البيانات، يتم ربط خصائص العنصر تلقائيًا بالمواقع الفرعية لقاعدة البيانات بطريقة متداخلة. إذا انتقلت الآن إلى عنوان URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name، ستظهر لك القيمة "Alan Turing". يمكنك أيضًا حفظ البيانات مباشرةً في موقع فرعي:
Java
usersRef.child("alanisawesome").setValueAsync(new User("June 23, 1912", "Alan Turing")); usersRef.child("gracehop").setValueAsync(new User("December 9, 1906", "Grace Hopper"));
Node.js
const usersRef = ref.child('users'); usersRef.child('alanisawesome').set({ date_of_birth: 'June 23, 1912', full_name: 'Alan Turing' }); usersRef.child('gracehop').set({ date_of_birth: 'December 9, 1906', full_name: 'Grace Hopper' });
Python
users_ref.child('alanisawesome').set({ 'date_of_birth': 'June 23, 1912', 'full_name': 'Alan Turing' }) users_ref.child('gracehop').set({ 'date_of_birth': 'December 9, 1906', 'full_name': 'Grace Hopper' })
متابعة
if err := usersRef.Child("alanisawesome")&.Set(ctx, User{ DateOfBirth: "June 23, 1912", FullName: "Alan Turing", }); err != nil { log.Fatalln("Error setting value:", err) } if err& := usersRef.Child("gracehop").Set(ctx, User{ DateOfBirth: "December 9, 1906", FullName: "Grace Hopper", }); err != nil { log.Fatalln("Error setting value:", err) }
سيؤدي المثالان أعلاه - كتابة كلتا القيمتين في نفس الوقت ككائن وكتابتهما بشكل منفصل في المواقع الفرعية - إلى حفظ نفس البيانات في قاعدة البيانات الخاصة بك:
{
"users": {
"alanisawesome": {
"date_of_birth": "June 23, 1912",
"full_name": "Alan Turing"
},
"gracehop": {
"date_of_birth": "December 9, 1906",
"full_name": "Grace Hopper"
}
}
}
سيؤدي المثال الأول إلى تشغيل حدث واحد فقط على البرامج التي تراقب البيانات، بينما سيؤدي المثال الثاني إلى تشغيل حدثين. يُرجى العِلم أنّه في حال توفُّر بيانات في usersRef، سيؤدي الأسلوب الأول إلى استبدالها، بينما سيؤدي الأسلوب الثاني إلى تعديل قيمة كل عقدة فرعية منفصلة فقط بدون تغيير العُقد الفرعية الأخرى في usersRef.
جارٍ تعديل البيانات المحفوظة
إذا أردت الكتابة إلى عدة عناصر فرعية لموقع قاعدة بيانات في الوقت نفسه بدون استبدال العُقد الفرعية الأخرى، يمكنك استخدام طريقة التعديل كما هو موضّح أدناه:
Java
DatabaseReference hopperRef = usersRef.child("gracehop&q<uot;); MapStri>ng, Object hopperUpdates = n<>ew HashMap(); hopperUpdates.put("nickname", "Amazing Grace"); hopperRef.updateChildrenAsync(hopperUpdates);
Node.js
const usersRef = ref.child('users'); const hopperRef = usersRef.child('gracehop'); hopperRef.update({ 'nickname': 'Amazing Grace' });
Python
hopper_ref = users_ref.child('gracehop') hopper_ref.update({ 'nickname': 'Amazing Grace' })
متابعة
hopperRef := usersRef.Child("gracehop") if err := hopperRef.Update(ctx, map[string]interface{}{ "nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating child:", err) }
سيؤدي ذلك إلى تعديل بيانات "غادة" لتشمل لقبها. إذا استخدمت set هنا بدلاً من update،
سيتم حذف كل من full_name وdate_of_birth من hopperRef.
يتيح Firebase Realtime Database أيضًا التعديلات المتعددة المسارات. وهذا يعني أنّ عملية التعديل يمكنها الآن تعديل القيم في مواقع متعددة في قاعدة البيانات في الوقت نفسه، وهي ميزة فعّالة تساعدك في إلغاء تسوية بياناتك. باستخدام التحديثات متعددة المسارات، يمكنك إضافة ألقاب إلى كل من غريس وآلان في نفس الوقت:
Java
Map<String, Object> userUpdates = new HashMap<>(); userUpdates.put("alanisawesome/nickname", "Alan The Machine"); userUpdates.put("gracehop/nickname", "Amazing Grace"); usersRef.updateChildrenAsync(userUpdates);
Node.js
const usersRef = ref.child('users'); usersRef.update({ 'alanisawesome/nickname': 'Alan The Machine', 'gracehop/nickname': 'Amazing Grace' });
Python
users_ref.update({ 'alanisawesome/nickname': 'Alan The Machine', 'gracehop/nickname': 'Amazing Grace' })
متابعة
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisawesome/nickname": "Alan The Machine", "gracehop/nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating children:", err) }
بعد هذا التعديل، تمت إضافة اللقبَين لكل من "أحمد" و"غادة":
{
"users": {
"alanisawesome": {
"date_of_birth": "June 23, 1912",
"full_name": "Alan Turing",
"nickname": "Alan The Machine"
},
"gracehop": {
"date_of_birth": "December 9, 1906",
"full_name": "Grace Hopper",
"nickname": "Amazing Grace"
}
}
}يُرجى العِلم أنّ محاولة تعديل العناصر من خلال كتابة عناصر تتضمّن المسارات ستؤدي إلى سلوك مختلف. لنلقِ نظرة على ما يحدث إذا حاولت بدلاً من ذلك تعديل Grace وAlan بهذه الطريقة:
Java
Map<String, Object> userNicknameUpdates = new HashMap<>(); userNicknameUpdates.put("alanisawesome", new User(null, null, "Alan The Machine")); userNicknameUpdates.put("gracehop", new User(null, null, "Amazing Grace")); usersRef.updateChildrenAsync(userNicknameUpdates);
Node.js
const usersRef = ref.child('users'); usersRef.update({ 'alanisawesome': { 'nickname': 'Alan The Machine' }, 'gracehop': { 'nickname': 'Amazing Grace' } });
Python
users_ref.update({ 'alanisawesome': { 'nickname': 'Alan The Machine' }, 'gracehop': { 'nickname': 'Amazing Grace' } })
متابعة
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisaweso&me": User{Nickname: "Alan The Machine"}&, "gracehop": User{Nickname: "Amazing Grace"}, }); err != nil { log.Fatalln("Error updating children:", err) }
يؤدي ذلك إلى سلوك مختلف، وهو استبدال عقدة /users بالكامل:
{
"users": {
"alanisawesome": {
"nickname": "Alan The Machine"
},
"gracehop": {
"nickname": "Amazing Grace"
}
}
}إضافة دالة ردّ نداء عند الاكتمال
في Node.js وJava مدير SDK، إذا أردت معرفة وقت تنفيذ بياناتك، يمكنك إضافة دالة استدعاء للإكمال. تتلقّى كل من طريقتَي الضبط والتعديل في حِزم SDK هذه دالة استدعاء اختيارية للإكمال يتم استدعاؤها عند تنفيذ عملية الكتابة في قاعدة البيانات. إذا لم تنجح المكالمة لسبب ما، يتم تمرير كائن خطأ إلى دالة الاستدعاء يشير إلى سبب حدوث الخطأ. في حِزم SDK لمشرفي Python وGo، يتم حظر جميع طرق الكتابة. وهذا يعني أنّ طرق الكتابة لا تظهر إلا بعد تنفيذ عمليات الكتابة في قاعدة البيانات.
Java
DatabaseReference dataRef = ref.child("data"); dataRef.setValue("I'm writing data", new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { if (databaseError != null) { System.out.println("Data could not be saved " + databaseError.getMessage()); } else { System.out.println("Data saved successfully."); } } });
Node.js
dataRef.set('I\'m writing data'>;, (error) = { if (error) { console.log('Data could not be saved.' + error); } else { console.log('Data saved successfully.'); } });
حفظ قوائم البيانات
عند إنشاء قوائم بيانات، من المهم مراعاة طبيعة معظم التطبيقات التي تتيح تعديل البيانات من قِبل عدة مستخدمين، وتعديل بنية القائمة وفقًا لذلك. للتوسّع في المثال أعلاه، لنضِف منشورات المدوّنة إلى تطبيقك. قد يكون حدسك الأول هو استخدام set لتخزين العناصر الفرعية مع فهارس أعداد صحيحة تزداد تلقائيًا، مثل ما يلي:
// NOT RECOMMENDED - use push() instead! { "posts": { "0": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "1": { "author": "alanisawesome", "title": "The Turing Machine" } } }
إذا أضاف مستخدم منشورًا جديدًا، سيتم تخزينه على النحو التالي: /posts/2. قد ينجح هذا الإجراء إذا كان هناك مؤلف واحد فقط يضيف المشاركات، ولكن في تطبيق التدوين التعاوني، قد يضيف العديد من المستخدمين مشاركات في الوقت نفسه. إذا
كتب مؤلفان في /posts/2 في الوقت نفسه، سيتم حذف إحدى المشاركتين من قِبل الأخرى.
لحلّ هذه المشكلة، توفّر برامج Firebase دالة push() تنشئ مفتاحًا فريدًا لكل عنصر فرعي جديد. باستخدام مفاتيح فرعية فريدة، يمكن لعدة عملاء إضافة عناصر فرعية إلى الموقع نفسه في الوقت نفسه بدون القلق بشأن تعارضات الكتابة.
Java
public static class Post { public String author; public String title; public Post(String author, String title) { // ... } } DatabaseReference postsRef = ref.child("posts"); DatabaseReference newPostRef = postsRef.push(); newPostRef.setValueAsync(new Post("gracehop", "Announcing COBOL, a New Programming Language")); // We can also chain the two calls together postsRef.push().setValueAsync(new Post("alanisawesome", "The Turing Machine"));
Node.js
const newPostRef = postsRef.push(); newPostRef.set({ author: 'gracehop', title: 'Announcing COBOL, a New Programming Language' }); // we can also chain the two calls together postsRef.push().set({ author: 'alanisawesome', title: 'The Turing Machine' });
Python
posts_ref = ref.child('posts') new_post_ref = posts_ref.push() new_post_ref.set({ 'author': 'gracehop', 'title': 'Announcing COBOL, a New Programming Language' }) # We can also chain the two calls together posts_ref.push().set({ 'author': 'alanisawesome', 'title': 'The Turing Machine' })
متابعة
// Post is a json-serializable type. type Post struct { Author string `json:"author,omitempty"` Title string `json:"title,omitempty"` } postsRef := ref.Child("posts") newPostRef, err := postsRef.Push(ctx, nil) if err != nil { log.Fatalln("Error pushing child node:"&, err) } if err := newPostRef.Set(ctx, Post{ Author: "gracehop", Title: "Announcing COBOL, a New Programming Language", }); err != nil { log.Fatalln("Error setting value:", err) } // We &can also chain the two calls together if _, err := postsRef.Push(ctx, Post{ Author: "alanisawesome", Title: "The Turing Machine", }); err != nil { log.Fatalln("Error pushing child node:", err) }
يعتمد المفتاح الفريد على طابع زمني، لذلك سيتم ترتيب عناصر القائمة تلقائيًا ترتيبًا زمنيًا. بما أنّ Firebase تنشئ مفتاحًا فريدًا لكل منشور مدوّنة، لن تحدث أي تعارضات في الكتابة إذا أضاف عدة مستخدمين منشورًا في الوقت نفسه. تبدو بيانات قاعدة البيانات الآن كما يلي:
{
"posts": {
"-JRHTHaIs-jNPLXOQivY": {
"author": "gracehop",
"title": "Announcing COBOL, a New Programming Language"
},
"-JRHTHaKuITFIhnj02kE": {
"author": "alanisawesome",
"title": "The Turing Machine"
}
}
}
في JavaScript وPython وGo، يكون نمط استدعاء push() ثم استدعاء set() على الفور شائعًا جدًا لدرجة أنّ حزمة تطوير البرامج (SDK) لمنصّة Firebase تتيح لك دمجهما من خلال تمرير البيانات التي سيتم ضبطها مباشرةً على push() على النحو التالي:
Java
// No Java equivalentNode.js
// This is equivalent to the calls to push().set(...) above postsRef.push({ author: 'gracehop', title: 'Announcing COBOL, a New Programming Language' });;
Python
# This is equivalent to the calls to push().set(...) above posts_ref.push({ 'author': 'gracehop', 'title': 'Announcing COBOL, a New Programming Language' })
متابعة
if _, err := postsRef.Push(ctx, &Post{ Author: "gracehop", Title: "Announcing COBOL, a New Programming Language", }); err != nil { log.Fatalln("Error pushing child node:", err) }
الحصول على المفتاح الفريد الذي تم إنشاؤه بواسطة push()
سيؤدي استدعاء push() إلى عرض مرجع لمسار البيانات الجديد، والذي يمكنك استخدامه للحصول على المفتاح أو ضبط البيانات عليه. سيؤدي الرمز التالي إلى الحصول على البيانات نفسها كما في المثال أعلاه، ولكن سيكون لدينا الآن إمكانية الوصول إلى المفتاح الفريد الذي تم إنشاؤه:
Java
// Generate a reference to a new location and add some data using push() DatabaseReference pushedPostRef = postsRef.push(); // Get the unique ID generated by a push() String postId = pushedPostRef.getKey();
Node.js
// Generate a reference to a new location and add some data using push() const newPostRef = postsRef.push(); // Get the unique key generated by push() const postId = newPostRef.key;
Python
# Generate a reference to a new location and add some data using push() new_post_ref = posts_ref.push() # Get the unique key generated by push() post_id = new_post_ref.key
متابعة
// Generate a reference to a new location and add some data using Push() newPostRef, err := postsRef.Push(ctx, nil) if err != nil { log.Fatalln("Error pushing child node:", err) } // Get the unique key generated by Push() postID := newPostRef.Key
كما ترى، يمكنك الحصول على قيمة المفتاح الفريد من مرجع push().
في القسم التالي حول استرداد البيانات، سنتعرّف على كيفية قراءة هذه البيانات من قاعدة بيانات Firebase.
حفظ بيانات المعاملات
عند العمل مع بيانات معقّدة يمكن أن تتلف بسبب التعديلات المتزامنة، مثل العدّادات المتزايدة، توفّر حزمة تطوير البرامج (SDK) عملية معاملة.
في Java وNode.js، يمكنك منح عملية المعاملة دالتي رد اتصال: دالة تحديث ودالة رد اتصال اختيارية عند الاكتمال. في Python وGo، تكون عملية المعاملة محظورة، وبالتالي لا تقبل سوى دالة التعديل.
تأخذ دالة التعديل الحالة الحالية للبيانات كوسيطة، ويجب أن تعرض الحالة الجديدة المطلوبة التي تريد كتابتها. على سبيل المثال، إذا أردت زيادة عدد التصويتات المؤيّدة على منشور مدوّنة معيّن، يمكنك كتابة معاملة مثل ما يلي:
Java
DatabaseReference upvotesRef = ref.child("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes"); upvotesRef.runTransaction(new Transaction.Handler() { @Override public Transaction.Result doTransaction(MutableData mutableData) { Integer currentValue = mutableData.getValue(Integer.class); if (currentValue == null) { mutableData.setValue(1); } else { mutableData.setValue(currentValue + 1); } return Transaction.success(mutableData); } @Override public void onComplete( DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) { System.out.println("Transaction completed"); } });
Node.js
const upvotesRef = db.ref('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes'); upvotesRef.transaction((current_>value) = { return (current_value || 0) + 1; });
Python
def increment_votes(current_value): return current_value + 1 if current_value else 1 upvotes_ref = db.reference('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes') try: new_vote_count = upvotes_ref.transaction(increment_votes) print('Transaction completed') except db.TransactionAbortedError: print('Transaction failed to commit')
متابعة
fn := func(t db.TransactionNode) (interface{}, error) { var currentValue int if err := t.Unmarshal(¤tValue); err != nil { return nil, err } return currentValue + 1, nil } ref := client.NewRef("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes") if err := ref.Transaction(ctx, fn); err != nil { log.Fatalln("Transaction failed to commit:", err) }
يتحقّق المثال أعلاه مما إذا كان العدّاد null أو لم تتم زيادته بعد، لأنّه يمكن استدعاء المعاملات باستخدام null في حال عدم كتابة قيمة تلقائية.
إذا تم تشغيل الرمز أعلاه بدون دالة معاملة وحاول عميلان زيادته
في الوقت نفسه، سيكتب كلاهما 1 كقيمة جديدة، ما يؤدي إلى زيادة واحدة بدلاً من اثنتين.
الاتصال بالشبكة والكتابة بلا إنترنت
يحتفظ عملاء Firebase Node.js وJava بنسختهم الداخلية الخاصة من أي بيانات نشطة. عند كتابة البيانات، تتم كتابتها في هذه النسخة المحلية أولاً. بعد ذلك، يزامن العميل هذه البيانات مع قاعدة البيانات ومع العملاء الآخرين على أساس "بذل قصارى الجهد".
نتيجةً لذلك، ستؤدي جميع عمليات الكتابة في قاعدة البيانات إلى بدء أحداث محلية على الفور، قبل كتابة أي بيانات في قاعدة البيانات. وهذا يعني أنّه عند كتابة تطبيق باستخدام Firebase، سيظل تطبيقك متجاوبًا بغض النظر عن وقت استجابة الشبكة أو الاتصال بالإنترنت.
بعد إعادة إمكانية الاتصال، سنتلقّى مجموعة الأحداث المناسبة لكي يتمكّن العميل من "مواكبة" حالة الخادم الحالية، بدون الحاجة إلى كتابة أي رمز مخصّص.
تأمين بياناتك
يحتوي Firebase Realtime Database على لغة أمان تتيح لك تحديد المستخدمين الذين لديهم إذن بالقراءة والكتابة في العُقَد المختلفة لبياناتك. يمكنك الاطّلاع على مزيد من المعلومات حول هذا الموضوع في مقالة تأمين بياناتك.