خواندن و نوشتن داده ها

(اختیاری) نمونه اولیه و تست با Firebase Emulator Suite

قبل از صحبت در مورد نحوه خواندن و نوشتن برنامه شما از پایگاه داده Realtime، بیایید مجموعه‌ای از ابزارهایی را که می‌توانید برای نمونه‌سازی اولیه و آزمایش عملکرد پایگاه داده Realtime استفاده کنید، معرفی کنیم: Firebase Emulator Suite. اگر در حال آزمایش مدل‌های داده مختلف، بهینه‌سازی قوانین امنیتی خود یا تلاش برای یافتن مقرون‌به‌صرفه‌ترین راه برای تعامل با back-end هستید، امکان کار به صورت محلی بدون استقرار سرویس‌های زنده می‌تواند ایده خوبی باشد.

یک شبیه‌ساز پایگاه داده بلادرنگ بخشی از مجموعه شبیه‌سازها است که به برنامه شما امکان می‌دهد با محتوا و پیکربندی پایگاه داده شبیه‌سازی شده شما و همچنین منابع پروژه شبیه‌سازی شده شما (توابع، سایر پایگاه‌های داده و قوانین امنیتی) تعامل داشته باشد. emulator_suite_short

استفاده از شبیه‌ساز پایگاه داده Realtime فقط شامل چند مرحله است:

  1. اضافه کردن یک خط کد به فایل پیکربندی آزمایشی برنامه برای اتصال به شبیه‌ساز.
  2. از ریشه دایرکتوری پروژه محلی خود، firebase emulators:start .
  3. طبق معمول، با استفاده از SDK پلتفرم Realtime Database یا با استفاده از API REST Realtime Database، از کد نمونه اولیه برنامه خود فراخوانی انجام دهید.

یک راهنمای کامل شامل پایگاه داده بلادرنگ و توابع ابری موجود است. همچنین می‌توانید نگاهی به مقدمه Emulator Suite بیندازید.

دریافت مرجع پایگاه داده

برای خواندن یا نوشتن داده‌ها از پایگاه داده، به یک نمونه از DatabaseReference نیاز دارید:

DatabaseReference ref = FirebaseDatabase.instance.ref();

نوشتن داده

این سند اصول اولیه خواندن و نوشتن داده‌های Firebase را پوشش می‌دهد.

داده‌های Firebase در یک DatabaseReference نوشته می‌شوند و با انتظار یا گوش دادن به رویدادهای منتشر شده توسط مرجع، بازیابی می‌شوند. رویدادها یک بار برای وضعیت اولیه داده‌ها و بار دیگر هر زمان که داده‌ها تغییر کنند، منتشر می‌شوند.

عملیات نوشتن پایه

برای عملیات نوشتن اولیه، می‌توانید از set() برای ذخیره داده‌ها در یک مرجع مشخص استفاده کنید و هر داده موجود در آن مسیر را جایگزین کنید. می‌توانید یک مرجع را به انواع زیر تنظیم کنید: String ، boolean ، int ، double ، Map ، List .

برای مثال، می‌توانید با استفاده از set() یک کاربر را به صورت زیر اضافه کنید:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

await ref.set({
  "name": "John",
  "age": 18,
  "address": {
    "line1": "100 Mountain View"
  }
});

استفاده از set() به این روش، داده‌ها را در مکان مشخص شده، از جمله هر گره فرزند، بازنویسی می‌کند. با این حال، شما همچنان می‌توانید یک فرزند را بدون بازنویسی کل شیء به‌روزرسانی کنید. اگر می‌خواهید به کاربران اجازه دهید پروفایل‌های خود را به‌روزرسانی کنند، می‌توانید نام کاربری را به صورت زیر به‌روزرسانی کنید:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

// Only update the age, leave the name and address!
await ref.update({
  "age": 19,
});

متد update() یک زیرمسیر به گره‌ها می‌پذیرد و به شما امکان می‌دهد چندین گره را در پایگاه داده به طور همزمان به‌روزرسانی کنید:

DatabaseReference ref = FirebaseDatabase.instance.ref("users");

await ref.update({
  "123/age": 19,
  "123/address/line1": "1 Mountain View",
});

خواندن داده‌ها

خواندن داده‌ها با گوش دادن به رویدادهای ارزشمند

برای خواندن داده‌ها در یک مسیر و گوش دادن به تغییرات، از ویژگی onValue از DatabaseReference برای گوش دادن به DatabaseEvent استفاده کنید.

شما می‌توانید از DatabaseEvent برای خواندن داده‌ها در یک مسیر مشخص، همانطور که در زمان رویداد وجود دارند، استفاده کنید. این رویداد یک بار با اتصال شنونده و بار دیگر هر بار که داده‌ها، از جمله هر فرزند، تغییر می‌کنند، فعال می‌شود. این رویداد دارای یک ویژگی snapshot است که شامل تمام داده‌های موجود در آن مکان، از جمله داده‌های فرزند است. اگر داده‌ای وجود نداشته باشد، ویژگی exists مربوط به snapshot برابر با false و ویژگی value آن برابر با null خواهد بود.

مثال زیر یک برنامه وبلاگ نویسی اجتماعی را نشان می‌دهد که جزئیات یک پست را از پایگاه داده بازیابی می‌کند:

DatabaseReference starCountRef =
        FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
    final data = event.snapshot.value;
    updateStarCount(data);
});

شنونده یک DataSnapshot دریافت می‌کند که شامل داده‌های موجود در مکان مشخص‌شده در پایگاه داده در زمان رویداد در ویژگی value آن است.

یک بار خواندن داده‌ها

یک بار خواندن با استفاده از get()

این SDK برای مدیریت تعاملات با سرورهای پایگاه داده، چه برنامه شما آنلاین باشد و چه آفلاین، طراحی شده است.

به طور کلی، شما باید از تکنیک‌های رویدادهای مقداری که در بالا توضیح داده شد برای خواندن داده‌ها استفاده کنید تا از به‌روزرسانی‌های داده‌ها از backend مطلع شوید. این تکنیک‌ها میزان استفاده و هزینه شما را کاهش می‌دهند و برای ارائه بهترین تجربه به کاربران شما در هنگام آنلاین و آفلاین بودن، بهینه شده‌اند.

اگر فقط یک بار به داده‌ها نیاز دارید، می‌توانید get() برای دریافت یک تصویر لحظه‌ای از داده‌ها از پایگاه داده استفاده کنید. اگر به هر دلیلی get() نتواند مقدار سرور را برگرداند، کلاینت حافظه پنهان محلی را بررسی می‌کند و اگر هنوز مقدار پیدا نشده باشد، خطا می‌دهد.

مثال زیر بازیابی نام کاربری عمومی یک کاربر را یک بار از پایگاه داده نشان می‌دهد:

final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
    print(snapshot.value);
} else {
    print('No data available.');
}

استفاده‌ی غیرضروری از get() می‌تواند استفاده از پهنای باند را افزایش داده و منجر به از دست رفتن عملکرد شود، که می‌توان با استفاده از یک شنونده‌ی بلادرنگ، همانطور که در بالا نشان داده شده است، از آن جلوگیری کرد.

خواندن داده‌ها یک بار با once()

در برخی موارد، ممکن است بخواهید مقدار از حافظه پنهان محلی بلافاصله برگردانده شود، به جای اینکه مقدار به‌روزرسانی‌شده در سرور بررسی شود. در این موارد، می‌توانید once() برای دریافت فوری داده‌ها از حافظه پنهان دیسک محلی استفاده کنید.

این برای داده‌هایی مفید است که فقط یک بار نیاز به بارگذاری دارند و انتظار نمی‌رود که مرتباً تغییر کنند یا نیاز به گوش دادن فعال داشته باشند. برای مثال، برنامه وبلاگ نویسی در مثال‌های قبلی از این روش برای بارگذاری پروفایل کاربر هنگام شروع نوشتن یک پست جدید استفاده می‌کند:

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

به‌روزرسانی یا حذف داده‌ها

به‌روزرسانی فیلدهای خاص

برای نوشتن همزمان در فرزندان خاص یک گره بدون بازنویسی سایر گره‌های فرزند، از متد update() استفاده کنید.

هنگام فراخوانی update() ، می‌توانید مقادیر فرزند سطح پایین‌تر را با مشخص کردن مسیری برای کلید به‌روزرسانی کنید. اگر داده‌ها برای مقیاس‌پذیری بهتر در چندین مکان ذخیره شده‌اند، می‌توانید تمام نمونه‌های آن داده‌ها را با استفاده از تابع data fan-out به‌روزرسانی کنید. به عنوان مثال، یک برنامه وبلاگ‌نویسی اجتماعی ممکن است بخواهد پستی ایجاد کند و همزمان آن را در فید فعالیت اخیر و فید فعالیت کاربر ارسال‌کننده به‌روزرسانی کند. برای انجام این کار، برنامه وبلاگ‌نویسی از کدی مانند این استفاده می‌کند:

void writeNewPost(String uid, String username, String picture, String title,
        String body) async {
    // A post entry.
    final postData = {
        'author': username,
        'uid': uid,
        'body': body,
        'title': title,
        'starCount': 0,
        'authorPic': picture,
    };

    // Get a key for a new Post.
    final newPostKey =
        FirebaseDatabase.instance.ref().child('posts').push().key;

    // Write the new post's data simultaneously in the posts list and the
    // user's post list.
    final Map<String, Map> updates = {};
    updates['/posts/$newPostKey'] = postData;
    updates['/user-posts/$uid/$newPostKey'] = postData;

    return FirebaseDatabase.instance.ref().update(updates);
}

این مثال از push() برای ایجاد یک پست در گره حاوی پست‌های همه کاربران در /posts/$postid استفاده می‌کند و همزمان کلید را با key بازیابی می‌کند. سپس می‌توان از این کلید برای ایجاد ورودی دوم در پست‌های کاربر در /user-posts/$userid/$postid استفاده کرد.

با استفاده از این مسیرها، می‌توانید به‌روزرسانی‌های همزمان را در چندین مکان در درخت JSON با یک فراخوانی واحد update() انجام دهید، مانند نحوه ایجاد پست جدید در هر دو مکان در این مثال. به‌روزرسانی‌های همزمان انجام شده به این روش اتمیک هستند: یا همه به‌روزرسانی‌ها موفق می‌شوند یا همه به‌روزرسانی‌ها شکست می‌خورند.

اضافه کردن یک فراخوانی تکمیل‌شده

اگر می‌خواهید بدانید چه زمانی داده‌هایتان ثبت شده‌اند، می‌توانید callbackهای تکمیل را ثبت کنید. هم set() و هم update() مقدار Future را برمی‌گردانند که می‌توانید callbackهای موفقیت و خطا را به آنها پیوست کنید که هنگام ثبت نوشتن در پایگاه داده و هنگام ناموفق بودن فراخوانی فراخوانی می‌شوند.

FirebaseDatabase.instance
    .ref('users/$userId/email')
    .set(emailAddress)
    .then((_) {
        // Data saved successfully!
    })
    .catchError((error) {
        // The write failed...
    });

حذف داده‌ها

ساده‌ترین راه برای حذف داده‌ها، فراخوانی تابع remove() روی ارجاعی به محل آن داده‌ها است.

همچنین می‌توانید با تعیین مقدار null به عنوان مقدار برای عملیات نوشتن دیگری مانند set() یا update() عملیات حذف را انجام دهید. می‌توانید از این تکنیک به همراه update() برای حذف چندین فرزند در یک فراخوانی API واحد استفاده کنید.

ذخیره داده‌ها به عنوان تراکنش

هنگام کار با داده‌هایی که ممکن است توسط تغییرات همزمان خراب شوند، مانند شمارنده‌های افزایشی، می‌توانید با ارسال یک کنترل‌کننده تراکنش به runTransaction() از یک تراکنش استفاده کنید. یک کنترل‌کننده تراکنش، وضعیت فعلی داده‌ها را به عنوان یک آرگومان دریافت می‌کند و وضعیت دلخواه جدیدی را که می‌خواهید بنویسید، برمی‌گرداند. اگر کلاینت دیگری قبل از نوشتن موفقیت‌آمیز مقدار جدید شما، در آن مکان بنویسد، تابع به‌روزرسانی شما دوباره با مقدار فعلی جدید فراخوانی می‌شود و نوشتن دوباره امتحان می‌شود.

برای مثال، در برنامه وبلاگ نویسی اجتماعی مثال زده شده، می‌توانید به کاربران اجازه دهید پست‌ها را ستاره‌دار و بدون ستاره کنند و تعداد ستاره‌های دریافتی یک پست را به صورت زیر پیگیری کنند:

void toggleStar(String uid) async {
  DatabaseReference postRef =
      FirebaseDatabase.instance.ref("posts/foo-bar-123");

  TransactionResult result = await postRef.runTransaction((Object? post) {
    // Ensure a post at the ref exists.
    if (post == null) {
      return Transaction.abort();
    }

    Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
    if (_post["stars"] is Map && _post["stars"][uid] != null) {
      _post["starCount"] = (_post["starCount"] ?? 1) - 1;
      _post["stars"][uid] = null;
    } else {
      _post["starCount"] = (_post["starCount"] ?? 0) + 1;
      if (!_post.containsKey("stars")) {
        _post["stars"] = {};
      }
      _post["stars"][uid] = true;
    }

    // Return the new data.
    return Transaction.success(_post);
  });
}

به طور پیش‌فرض، رویدادها هر بار که تابع به‌روزرسانی تراکنش اجرا می‌شود، رخ می‌دهند، بنابراین اگر تابع را چندین بار اجرا کنید، ممکن است حالت‌های میانی را ببینید. می‌توانید applyLocally روی false تنظیم کنید تا این حالت‌های میانی را سرکوب کنید و در عوض منتظر بمانید تا تراکنش کامل شود و سپس رویدادها رخ دهند:

await ref.runTransaction((Object? post) {
  // ...
}, applyLocally: false);

نتیجه یک تراکنش، یک TransactionResult است که شامل اطلاعاتی مانند اینکه آیا تراکنش انجام شده است یا خیر و snapshot جدید است:

DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");

TransactionResult result = await ref.runTransaction((Object? post) {
  // ...
});

print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot

لغو یک تراکنش

اگر می‌خواهید یک تراکنش را با خیال راحت لغو کنید، Transaction.abort() را فراخوانی کنید تا یک AbortTransactionException ایجاد شود:

TransactionResult result = await ref.runTransaction((Object? user) {
  if (user !== null) {
    return Transaction.abort();
  }

  // ...
});

print(result.committed); // false

افزایش‌های اتمی سمت سرور

در مورد استفاده بالا، ما دو مقدار را در پایگاه داده می‌نویسیم: شناسه کاربری که پست را ستاره‌دار/حذف ستاره می‌کند، و تعداد ستاره‌های افزایشی. اگر از قبل بدانیم که کاربر در حال ستاره‌دار کردن پست است، می‌توانیم به جای تراکنش از یک عملیات افزایش اتمی استفاده کنیم.

void addStar(uid, key) async {
  Map<String, Object?> updates = {};
  updates["posts/$key/stars/$uid"] = true;
  updates["posts/$key/starCount"] = ServerValue.increment(1);
  updates["user-posts/$key/stars/$uid"] = true;
  updates["user-posts/$key/starCount"] = ServerValue.increment(1);
  return FirebaseDatabase.instance.ref().update(updates);
}

این کد از عملیات تراکنش استفاده نمی‌کند، بنابراین در صورت وجود تداخل در به‌روزرسانی، به طور خودکار دوباره اجرا نمی‌شود. با این حال، از آنجایی که عملیات افزایش مستقیماً روی سرور پایگاه داده اتفاق می‌افتد، هیچ احتمالی برای تداخل وجود ندارد.

اگر می‌خواهید تداخل‌های خاص برنامه را شناسایی و رد کنید، مانند اینکه کاربری پستی را که قبلاً ستاره‌گذاری کرده است، ستاره‌گذاری کند، باید قوانین امنیتی سفارشی برای آن مورد استفاده بنویسید.

کار با داده‌ها به صورت آفلاین

اگر یک کلاینت اتصال شبکه خود را از دست بدهد، برنامه شما به درستی به کار خود ادامه خواهد داد.

هر کلاینت متصل به پایگاه داده Firebase، نسخه داخلی خود را از هر داده فعالی نگهداری می‌کند. وقتی داده‌ها نوشته می‌شوند، ابتدا در این نسخه محلی نوشته می‌شوند. سپس کلاینت Firebase آن داده‌ها را با سرورهای پایگاه داده راه دور و با سایر کلاینت‌ها بر اساس "بهترین تلاش" همگام‌سازی می‌کند.

در نتیجه، تمام نوشته‌ها در پایگاه داده، بلافاصله قبل از اینکه داده‌ای در سرور نوشته شود، رویدادهای محلی را فعال می‌کنند. این بدان معناست که برنامه شما صرف نظر از تأخیر شبکه یا اتصال، پاسخگو باقی می‌ماند.

پس از برقراری مجدد اتصال، برنامه شما مجموعه مناسبی از رویدادها را دریافت می‌کند تا کلاینت بدون نیاز به نوشتن هیچ کد سفارشی، با وضعیت فعلی سرور همگام‌سازی شود.

ما در بخش «درباره قابلیت‌های آنلاین و آفلاین بیشتر بدانید» درباره رفتار آفلاین بیشتر صحبت خواهیم کرد.

مراحل بعدی