يتناول هذا المستند الطرق الأربع لكتابة البيانات في Firebase Realtime Database: ضبط البيانات وتعديلها ودفعها ودعم المعاملات.
طرق حفظ البيانات
ضبط | كتابة البيانات أو استبدالها في مسار محدّد، مثل messages/users/<username> |
تعديل | تعديل بعض المفاتيح لمسار محدَّد بدون استبدال جميع البيانات |
دفع | الإضافة إلى قائمة بيانات في قاعدة البيانات في كل مرة تُضيف فيها عقدة جديدة إلى قائمة، تنشئ قاعدة بياناتك مفتاحًا فريدًا، مثل messages/users/<unique-user-id>/<username> . |
عملية | استخدام المعاملات عند التعامل مع بيانات معقدة يمكن أن تتلف بسبب التحديثات المتزامنة |
حفظ البيانات
عملية الكتابة الأساسية لقاعدة البيانات هي مجموعة تحفظ البيانات الجديدة في مرجع قاعدة البيانات المحدد، وتستبدل أي بيانات موجودة في هذا المسار. لفهم مجموعة البيانات، سننشئ تطبيق تدوين بسيطًا. يتم تخزين بيانات تطبيقك في مرجع قاعدة البيانات هذا:
جافا
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
إلى إزالة البيانات المتوفّرة في الموقع الجغرافي المحدّد. في هذه الحالة، ستقوم بتمريره كائنًا:
جافا
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' } })
انتقال
// 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". يمكنك أيضًا حفظ البيانات مباشرةً في موقع جغرافي لطفلك:
جافا
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
بدون تغيير.
تعديل البيانات المحفوظة
إذا أردت الكتابة إلى عدة عناصر ثانوية في موقع قاعدة بيانات في الوقت نفسه بدون استبدال العُقد الفرعية الأخرى، يمكنك استخدام طريقة التعديل كما هو موضّح أدناه:
جافا
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' })
انتقال
hopperRef := usersRef.Child("gracehop") if err := hopperRef.Update(ctx, map[string]interface{}{ "nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating child:", err) }
سيؤدي ذلك إلى تعديل بيانات "منال" لتضمين اسمها المعرِّف. إذا كنت قد استخدمت set here بدلاً من update،
لكان قد تم حذف كل من full_name
وdate_of_birth
من hopperRef
.
يتيح Firebase Realtime Database أيضًا تحديثات متعددة المسارات. وهذا يعني أنّه يمكن الآن إجراء تعديل على القيم في مواقع متعددة في قاعدة بياناتك في الوقت نفسه، وهي ميزة فعّالة تتيح لك إزالة الترتيب الطبيعي لبياناتك. باستخدام التعديلات على مسارات متعددة، يمكنك إضافة ألقاب لكل من "كريمة" و"أمير" في الوقت نفسه:
جافا
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" } } }
يُرجى العِلم أنّ محاولة تعديل العناصر عن طريق كتابة عناصر تتضمّن المسارات المضمّنة سيؤدي إلى سلوك مختلف. لنلقِ نظرة على ما سيحدث إذا حاولت تعديل معلومات "غريس" و"ألان" بهذه الطريقة:
جافا
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{}{ "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، تكون جميع طرق الكتابة محظورة. أي أنّ طُرق الكتابة لا يتم عرضها إلا بعد أن تكون عمليات الكتابة ملتزمة بقاعدة البيانات.
جافا
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()
تنشئ
مفتاحًا فريدًا لكلّ طفل جديد. باستخدام مفاتيح فرعية فريدة، يمكن لعدة عملاء
إضافة عناصر فرعية إلى الموقع الجغرافي نفسه في الوقت نفسه بدون القلق بشأن تعارضات الكتابة.
جافا
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()
على النحو التالي:
جافا
// 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' })
انتقال
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()
إلى عرض مرجع إلى مسار البيانات الجديد، والذي يمكنك استخدامه للحصول على المفتاح أو ضبط البيانات عليه. سيؤدي استخدام الرمز البرمجي التالي إلى عرض البيانات نفسها الواردة في المثال أعلاه، ولكن يمكننا الآن الوصول إلى المفتاح الفريد الذي تم إنشاؤه:
جافا
// 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، تكون عملية المعاملة حظرًا وبالتالي لا تقبل سوى دالة update.
تأخذ دالة update الحالة الحالية للبيانات كوسيطة، ويجب أن تُرجع الحالة الجديدة المطلوبة التي تريد كتابتها. على سبيل المثال، إذا أردت زيادة عدد الأصوات التي حصلت عليها مشاركة مدونة معيّنة، يمكنك كتابة معاملة على النحو التالي:
جافا
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
كقيمة جديدة، ما يؤدي إلى زيادة واحدة بدلاً من زيادتَين.
الاتصال بالشبكة وعمليات الكتابة بلا إنترنت
يحتفظ عملاء Node.js وJava من Firebase بإصدار داخلي خاص من أي بيانات نشطة. عند كتابة البيانات، تتم أولاً كتابتها في هذا الإصدار على الجهاز. بعد ذلك، يُزامن العميل هذه البيانات مع قاعدة البيانات ومع العملاء الآخرين على أساس "أحسن ما يمكن".
نتيجةً لذلك، ستؤدي جميع عمليات الكتابة إلى قاعدة البيانات إلى بدء الأحداث المحلية على الفور، قبل كتابة أي بيانات في قاعدة البيانات. وهذا يعني أنّه عند كتابة تطبيق باستخدام Firebase، سيظل تطبيقك سريع الاستجابة بغض النظر عن وقت استجابة الشبكة أو اتصال الإنترنت.
بعد إعادة إنشاء الاتصال، سنتلقّى مجموعة الأحداث المناسبة لكي "يواكب" العميل حالة الخادم الحالية، بدون الحاجة إلى كتابة أي رمز مخصّص.
تأمين بياناتك
تشمل Firebase Realtime Database لغة أمان تتيح لك تحديد المستخدمين الذين لديهم الإذن بالقراءة والكتابة في العُقد المختلفة لبياناتك. يمكنك الاطّلاع على مزيد من المعلومات حول هذا الموضوع في مقالة تأمين بياناتك.