این سند چهار روش برای نوشتن دادهها در 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");
// 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');
# 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 به set ارسال کنید. ارسال 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);
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' } });
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 در پایگاه داده ذخیره میشود، ویژگیهای شیء به طور خودکار به مکانهای فرزند پایگاه داده به صورت تو در تو نگاشت میشوند. اکنون اگر به آدرس اینترنتی 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"));
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' });
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 را بدون تغییر باقی میگذارد.
اگر میخواهید همزمان روی چندین فرزند از یک مکان پایگاه داده بنویسید بدون اینکه سایر گرههای فرزند را رونویسی کنید، میتوانید از متد update مطابق شکل زیر استفاده کنید:
DatabaseReference hopperRef = usersRef.child("gracehop"); Map<String, Object> hopperUpdates = new HashMap<>(); hopperUpdates.put("nickname", "Amazing Grace"); hopperRef.updateChildrenAsync(hopperUpdates);
const usersRef = ref.child('users'); const hopperRef = usersRef.child('gracehop'); hopperRef.update({ 'nickname': 'Amazing Grace' });
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) }
این کار دادههای گریس را بهروزرسانی میکند تا نام مستعار او را نیز شامل شود. اگر به جای update از set در اینجا استفاده میکردید، هم full_name و هم date_of_birth از hopperRef شما حذف میشدند.
Firebase Realtime Database همچنین از بهروزرسانیهای چند مسیری پشتیبانی میکند. این بدان معناست که اکنون update میتواند مقادیر را در چندین مکان در پایگاه داده شما به طور همزمان بهروزرسانی کند، یک ویژگی قدرتمند که به شما کمک میکند دادههای خود را از حالت نرمال خارج کنید . با استفاده از بهروزرسانیهای چند مسیری، میتوانید همزمان به Grace و Alan نام مستعار اضافه کنید:
Map<String, Object> userUpdates = new HashMap<>(); userUpdates.put("alanisawesome/nickname", "Alan The Machine"); userUpdates.put("gracehop/nickname", "Amazing Grace"); usersRef.updateChildrenAsync(userUpdates);
const usersRef = ref.child('users'); usersRef.update({ 'alanisawesome/nickname': 'Alan The Machine', 'gracehop/nickname': 'Amazing Grace' });
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);
const usersRef = ref.child('users'); usersRef.update({ 'alanisawesome': { 'nickname': 'Alan The Machine' }, 'gracehop': { 'nickname': 'Amazing Grace' } });
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 Admin، اگر میخواهید بدانید چه زمانی دادههای شما ثبت شدهاند، میتوانید یک تابع فراخوانی تکمیل (commission callback) اضافه کنید. هر دو متد set و update در این SDK ها یک تابع فراخوانی تکمیل اختیاری میگیرند که وقتی نوشتن در پایگاه داده ثبت شده است، فراخوانی میشود. اگر به هر دلیلی فراخوانی ناموفق باشد، یک شیء error به تابع فراخوانی ارسال میشود که نشان میدهد چرا این شکست رخ داده است. در SDK های Python و Go Admin، همه متدهای نوشتن مسدود میشوند. یعنی متدهای نوشتن تا زمانی که نوشتنها در پایگاه داده ثبت نشوند، بر نمیگردند.
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."); } } });
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() ارائه میدهند که برای هر فرزند جدید یک کلید منحصر به فرد تولید میکند . با استفاده از کلیدهای فرزند منحصر به فرد، چندین کلاینت میتوانند فرزندان را همزمان به یک مکان اضافه کنند، بدون اینکه نگران تداخل در نوشتن باشند.
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"));
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' });
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"
}
}
}در جاوا اسکریپت، پایتون و گو، الگوی فراخوانی push() و سپس بلافاصله فراخوانی set() آنقدر رایج است که فایربیس SDK به شما امکان میدهد با ارسال مستقیم دادههایی که قرار است تنظیم شوند به push() به صورت زیر، آنها را ترکیب کنید:
// No Java equivalent// This is equivalent to the calls to push().set(...) above postsRef.push({ author: 'gracehop', title: 'Announcing COBOL, a New Programming Language' });;
# 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() یک ارجاع به مسیر داده جدید را برمیگرداند که میتوانید از آن برای دریافت کلید یا تنظیم دادهها در آن استفاده کنید. کد زیر همان دادههای مثال بالا را نتیجه میدهد، اما اکنون به کلید منحصر به فردی که تولید شده است دسترسی خواهیم داشت:
// 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();
// 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;
# 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 یک عملیات تراکنش ارائه میدهد.
در جاوا و Node.js، شما به عملیات تراکنش دو تابع فراخوانی میدهید: یک تابع بهروزرسانی و یک تابع فراخوانی تکمیل اختیاری. در پایتون و گو، عملیات تراکنش مسدود میشود و بنابراین فقط تابع بهروزرسانی را میپذیرد.
تابع بهروزرسانی، وضعیت فعلی دادهها را به عنوان آرگومان دریافت میکند و باید وضعیت دلخواه جدیدی را که میخواهید بنویسید، برگرداند. برای مثال، اگر میخواهید تعداد رأیهای مثبت به یک پست وبلاگ خاص را افزایش دهید، باید تراکنشی مانند زیر بنویسید:
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"); } });
const upvotesRef = db.ref('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes'); upvotesRef.transaction((current_value) => { return (current_value || 0) + 1; });
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 و جاوا، نسخه داخلی خود را از هرگونه داده فعال نگهداری میکنند. وقتی دادهای نوشته میشود، ابتدا در این نسخه محلی نوشته میشود. سپس کلاینت، آن دادهها را با پایگاه داده و با سایر کلاینتها بر اساس «بهترین تلاش» همگامسازی میکند.
در نتیجه، تمام نوشتهها در پایگاه داده، بلافاصله و قبل از اینکه هیچ دادهای در پایگاه داده نوشته شود، رویدادهای محلی را فعال میکنند. این بدان معناست که وقتی با استفاده از Firebase برنامهای مینویسید، برنامه شما صرف نظر از تأخیر شبکه یا اتصال به اینترنت، پاسخگو خواهد ماند.
پس از برقراری مجدد اتصال، مجموعه رویدادهای مناسب را دریافت خواهیم کرد تا کلاینت بدون نیاز به نوشتن هیچ کد سفارشی، با وضعیت فعلی سرور «هماهنگ» شود.
Firebase Realtime Database دارای یک زبان امنیتی است که به شما امکان میدهد تعریف کنید کدام کاربران به گرههای مختلف دادههای شما دسترسی خواندن و نوشتن دارند. میتوانید اطلاعات بیشتر در مورد آن را در بخش «دادههای خود را ایمن کنید» مطالعه کنید.