جارٍ حفظ البيانات

يتناول هذا المستند الطرق الأربع لكتابة البيانات في قاعدة بيانات Firebase في الوقت الفعلي: دعم الإعداد والتحديث والدفع والمعاملات.

طرق حفظ البيانات

ضبط كتابة البيانات أو استبدالها في مسار محدّد، مثل 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() / 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/save-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)
}

سيؤدي هذا إلى تحديث بيانات غريس لتضمين لقبها. إذا كنت قد استخدمت ضبط هنا بدلاً من التعديل، سيتم حذف كل من full_name وdate_of_birth من hopperRef.

تتوافق قاعدة بيانات Firebase في الوقت الفعلي أيضًا مع التحديثات المتعدّدة المسارات. يعني ذلك أنّه يمكن الآن لهذا التعديل تعديل القيم في مواقع جغرافية متعددة في قاعدة البيانات في الوقت نفسه، وهي ميزة فعّالة تتيح لك إلغاء تطبيع بياناتك. باستخدام التحديثات متعددة المسارات، يمكنك إضافة ألقاب إلى كل من "غريس" و"سمير" في الوقت نفسه:

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"
    }
  }
}

لاحظ أن محاولة تحديث الكائنات عن طريق كتابة الكائنات مع المسارات المضمنة ستؤدي إلى سلوك مختلف. لنلقِ نظرة على ما يحدث إذا حاولت تحديث "غريس" و"آلان" بهذه الطريقة:

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) لمشرفي Java وNode.js، يمكنك إضافة طلب استدعاء إكمال إذا أردت معرفة تاريخ اكتمال البيانات. في حِزم تطوير البرامج (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، يتم استدعاء عمليتَي استدعاء: دالة تحديث وعملية استدعاء اختيارية لإكمال العملية. في بايثون وغو، يتم حظر العملية، وبالتالي لا يتم قبول سوى دالة التحديث.

تستخدم دالة التعديل الحالة الحالية للبيانات كوسيطة، ومن المفترض أن تعرض الحالة الجديدة المطلوبة التي تريد كتابتها. على سبيل المثال، إذا أردت زيادة عدد التصويتات المؤيِّدة على مشاركة مدونة معيّنة، عليك كتابة معاملة على النحو التالي:

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(&currentValue); 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 في Firebase وJava بنسختهم الداخلية الخاصة من أي بيانات نشطة. وعند كتابة البيانات، تتم كتابتها أولاً على هذا الإصدار المحلي. ويجري البرنامج بعد ذلك مزامنة تلك البيانات مع قاعدة البيانات ومع البرامج الأخرى على أساس "أفضل جهد".

ونتيجةً لذلك، تؤدي جميع عمليات الكتابة في قاعدة البيانات إلى تشغيل الأحداث المحلية على الفور، قبل كتابة أي بيانات في قاعدة البيانات. وهذا يعني أنّه عند كتابة تطبيق باستخدام Firebase، سيظل تطبيقك سريع الاستجابة بغض النظر عن وقت استجابة الشبكة أو الاتصال بالإنترنت.

بعد إعادة الاتصال، سنرسل مجموعة مناسبة من الأحداث لكي "يتابع" العميل حالة الخادم الحالية بدون الحاجة إلى كتابة أي رمز مخصّص.

تأمين بياناتك

تتضمن قاعدة بيانات Firebase في الوقت الفعلي لغة أمان تتيح لك تحديد المستخدمين الذين لديهم إمكانية الوصول إلى عُقد مختلفة من بياناتك للقراءة والكتابة. يمكنك قراءة المزيد من المعلومات حول ذلك في قسم تأمين بياناتك.