حفظ البيانات

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

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

ضبط كتابة البيانات أو استبدالها في مسار محدّد، مثل 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 هنا بدلاً من طريقة الإرسال نظرًا لأنك تمتلك المفتاح ولا تحتاج إلى إنشاء واحد.

أولاً، قم بإنشاء مرجع قاعدة بيانات لبيانات المستخدم الخاصة بك. استخدِم بعد ذلك 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)
}

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

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

جافا
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 بدلاً من ذلك بهذه الطريقة:

جافا
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"
    }
  }
}

إضافة معاودة اتصال مكتملة

في Node.js وحزم SDK لمشرفي Java، إذا أردت معرفة وقت الالتزام ببياناتك، يمكنك إضافة طلب معاودة الاتصال لإكمال العملية. تتطلب كل من طريقتي الإعداد والتحديث في حِزم تطوير البرامج (SDK) هذه استدعاء إكمال اختياريًا، وهو ما يُعرف باسم عندما تلتزم عملية الكتابة بقاعدة البيانات. إذا لم ينجح الاتصال لبعض السبب، يتم تمرير كائن خطأ إلى معاودة الاتصال للإشارة إلى سبب حدوث الفشل. في حزم تطوير البرامج (SDK) للمشرف في Python وGo، يتم حظر جميع طرق الكتابة. أي أن طرق write لا تُرجع حتى تلتزم عمليات الكتابة بقاعدة البيانات.

جافا
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، يجب منح عملية المعاملة دالتَي استدعاء: دالة تحديث واستدعاء إكمال اختياري. في بايثون وGo، تؤدي عملية المعاملة إلى حظر ولذلك فهي تقبل دالة التحديث فقط.

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

جافا
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(&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 في الوقت الفعلي لغة أمان تتيح لك تحديد المستخدمين الذين لديهم حق الوصول للقراءة والكتابة إلى العقد المختلفة من بشكل أفضل. يمكنك الاطّلاع على المزيد من المعلومات في مقالة تأمين بياناتك.