حفظ البيانات

يتناول هذا المستند الطرق الأربع لكتابة البيانات في Firebase Realtime Database، وهي: set وupdate وpush ودعم المعاملات.

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

set كتابة البيانات أو استبدالها في مسار محدّد، مثل messages/users/<username>
update تعديل بعض المفاتيح لمسار محدّد بدون استبدال جميع البيانات
push إضافة البيانات إلى قائمة في قاعدة البيانات. في كل مرة تُرسِل فيها عُقدة جديدة إلى قائمة، تنشئ قاعدة البيانات مفتاحًا فريدًا، مثل messages/users/<unique-user-id>/<username>
transaction استخدام المعاملات عند التعامل مع البيانات المعقّدة التي قد تتلف بسبب التعديلات المتزامنة

حفظ البيانات

إنّ عملية كتابة البيانات الأساسية في قاعدة البيانات هي عملية set التي تحفظ بيانات جديدة في مرجع قاعدة البيانات المحدّد، ما يؤدي إلى استبدال أي بيانات حالية في هذا المسار. لفهم عملية set، سننشئ تطبيقًا بسيطًا للتدوين. يتم تخزين بيانات تطبيقك في مرجع قاعدة البيانات هذا:

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 إلى set. سيؤدي تمرير 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 بدون تغيير.

تعديل البيانات المحفوظة

إذا أردت الكتابة إلى عدة عناصر ثانوية لموقع قاعدة بيانات في الوقت نفسه بدون استبدال عُقد العناصر الثانوية الأخرى ، يمكنك استخدام طريقة update كما هو موضّح أدناه:

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

سيؤدي ذلك إلى تعديل بيانات Grace لتضمين لقبها. إذا كنت قد استخدمت 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)
}

بعد هذا التعديل، تمت إضافة الألقاب إلى كل من Alan وGrace:

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

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

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

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.');
  }
});

حفظ قوائم البيانات

عند إنشاء قوائم بيانات، من المهم مراعاة طبيعة التطبيقات المتعددة المستخدمين في معظم التطبيقات و تعديل بنية القائمة وفقًا لذلك. بالاستناد إلى المثال أعلاه، لنضِف مشاركات مدونة إلى تطبيقك. قد يكون أول ما يخطر ببالك هو استخدام set لتخزين العناصر الثانوية باستخدام فهارس عددية متزايدة تلقائيًا، مثل ما يلي:

// 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 كقيمة جديدة، ما يؤدي إلى زيادة واحدة بدلاً من اثنتَين.

الاتصال بالشبكة وعمليات الكتابة في وضع عدم الاتصال بالإنترنت

تحتفظ برامج Firebase Node.js وJava للعملاء بنسختها الداخلية من أي بيانات نشطة. عند كتابة البيانات، يتم كتابتها في هذه النسخة المحلية أولاً. بعد ذلك، يزامن العميل هذه البيانات مع قاعدة البيانات ومع العملاء الآخرين على أساس "بذل أفضل جهد".

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

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

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

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