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

يتناول هذا المستند الطرق الأربعة لكتابة البيانات في قاعدة بيانات 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/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)
}

سيؤدي ذلك إلى تعديل بيانات "غريس" لتشمل لقبها. إذا كنت قد استخدمت ضبط هنا بدلاً من التعديل، كان هذا الإجراء سيؤدي إلى حذف كل من 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"
    }
  }
}

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

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

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

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

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