يتناول هذا المستند الطرق الأربع لكتابة البيانات في Firebase Realtime Database: set وupdate وpush وإتاحة المعاملات.
طرق توفير البيانات
ضبط | كتابة البيانات أو استبدالها في مسار محدّد، مثل messages/users/<username> |
تعديل | تعديل بعض المفاتيح لمسار محدّد بدون استبدال كل البيانات |
إرسال | إضافة إلى قائمة بيانات في قاعدة البيانات في كل مرة تُضيف فيها عقدة جديدة إلى قائمة، تنشئ قاعدة البيانات مفتاحًا فريدًا، مثل messages/users/<unique-user-id>/<username> |
عملية | استخدام المعاملات عند التعامل مع البيانات المعقّدة التي قد تتلف بسبب التعديلات المتزامنة |
حفظ البيانات
عملية الكتابة الأساسية في قاعدة البيانات هي عملية ضبط تحفظ البيانات الجديدة في مرجع قاعدة البيانات المحدّد، ما يؤدي إلى استبدال أي بيانات حالية في هذا المسار. لفهم عملية الضبط، سننشئ تطبيق تدوين بسيطًا. يتم تخزين بيانات تطبيقك في مرجع قاعدة البيانات هذا:
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')
Go
// 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"); Map<String, User> users = new 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' } })
Go
// 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' })
Go
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"); Map<String, Object> hopperUpdates = new 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' })
Go
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 أيضًا إجراء تحديثات متعددة المسارات. وهذا يعني أنّه يمكن الآن استخدام الأمر update لتعديل القيم في مواقع متعددة في قاعدة البيانات في الوقت نفسه، وهي ميزة فعّالة تساعدك في إزالة التسوية من بياناتك. باستخدام التعديلات المتعددة المسارات، يمكنك إضافة أسماء مستعارة إلى كل من Grace وAlan في الوقت نفسه:
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' })
Go
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' } })
Go
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisawesome": &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" } } }
إضافة دالة معاودة الاتصال عند اكتمال العملية
في حِزم تطوير البرامج (SDK) الخاصة بالمشرفين في Node.js وJava، يمكنك إضافة دالة ردّ الاتصال عند الاكتمال إذا أردت معرفة وقت إتمام عملية حفظ بياناتك. تتضمّن كلّ من طريقتَي الضبط والتعديل في حِزم 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.'); } });
حفظ قوائم البيانات
عند إنشاء قوائم بيانات، من المهم مراعاة طبيعة التطبيقات التي يستخدمها عدة مستخدمين وتعديل بنية القائمة وفقًا لذلك. بالاستناد إلى المثال أعلاه، لنفترض أنّك تريد إضافة مشاركات مدوّنة إلى تطبيقك. قد يكون أول ما يخطر ببالك هو استخدام مجموعة لتخزين العناصر الثانوية مع فهارس عدد صحيح تزداد تلقائيًا، مثل ما يلي:
// 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' })
Go
// 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 equivalent
Node.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' })
Go
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
Go
// 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')
Go
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 لغة أمان تتيح لك تحديد المستخدمين الذين لديهم إذن قراءة وكتابة لمختلف عُقد بياناتك. يمكنك الاطّلاع على مزيد من المعلومات حول هذا الموضوع في مقالة الحفاظ على أمان بياناتك.