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

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

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

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

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

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

یک راهنمای کامل شامل Realtime Database و Cloud Functions موجود است. همچنین می‌توانید نگاهی به مقدمه Local Emulator Suite بیندازید.

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

برای خواندن یا نوشتن داده‌ها از پایگاه داده، به یک نمونه از firebase.database.Reference نیاز دارید:

Web

import { getDatabase } from "firebase/database";

const database = getDatabase();

Web

var database = firebase.database();

نوشتن داده

این سند اصول اولیه بازیابی داده‌ها و نحوه مرتب‌سازی و فیلتر کردن داده‌های Firebase را پوشش می‌دهد.

داده‌های Firebase با اتصال یک شنونده‌ی ناهمزمان به firebase.database.Reference بازیابی می‌شوند. شنونده یک بار برای وضعیت اولیه‌ی داده‌ها و بار دیگر هر زمان که داده‌ها تغییر کنند، فعال می‌شود.

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

برای عملیات نوشتن اولیه، می‌توانید از set() برای ذخیره داده‌ها در یک مرجع مشخص استفاده کنید و هر داده موجود در آن مسیر را جایگزین کنید. به عنوان مثال، یک برنامه وبلاگ نویسی اجتماعی ممکن است یک کاربر را با set() به صورت زیر اضافه کند:

Web

import { getDatabase, ref, set } from "firebase/database";

function writeUserData(userId, name, email, imageUrl) {
  const db = getDatabase();
  set(ref(db, 'users/' + userId), {
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

Web

function writeUserData(userId, name, email, imageUrl) {
  firebase.database().ref('users/' + userId).set({
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

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

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

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

برای خواندن داده‌ها در یک مسیر و گوش دادن به تغییرات، از onValue() برای مشاهده رویدادها استفاده کنید. می‌توانید از این رویداد برای خواندن عکس‌های لحظه‌ای استاتیک از محتویات یک مسیر مشخص، همانطور که در زمان رویداد وجود داشته‌اند، استفاده کنید. این متد یک بار زمانی که شنونده متصل می‌شود و بار دیگر هر بار که داده‌ها، از جمله فرزندان، تغییر می‌کنند، فعال می‌شود. فراخوانی رویداد، یک عکس لحظه‌ای حاوی تمام داده‌های آن مکان، از جمله داده‌های فرزند، ارسال می‌کند. اگر داده‌ای وجود نداشته باشد، عکس لحظه‌ای هنگام فراخوانی exists() false و هنگام فراخوانی val() null را برمی‌گرداند.

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

Web

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const starCountRef = ref(db, 'posts/' + postId + '/starCount');
onValue(starCountRef, (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

Web

var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

شنونده (listener) یک snapshot دریافت می‌کند که شامل داده‌های موجود در مکان مشخص شده در پایگاه داده در زمان وقوع رویداد است. می‌توانید داده‌های موجود در snapshot را با متد val() بازیابی کنید.

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

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

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

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

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

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

Web

import { getDatabase, ref, child, get } from "firebase/database";

const dbRef = ref(getDatabase());
get(child(dbRef, `users/${userId}`)).then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

Web

const dbRef = firebase.database().ref();
dbRef.child("users").child(userId).get().then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

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

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

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

Web

import { getDatabase, ref, onValue } from "firebase/database";
import { getAuth } from "firebase/auth";

const db = getDatabase();
const auth = getAuth();

const userId = auth.currentUser.uid;
return onValue(ref(db, '/users/' + userId), (snapshot) => {
  const username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
}, {
  onlyOnce: true
});

Web

var userId = firebase.auth().currentUser.uid;
return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => {
  var username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
});

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

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

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

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

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

Web

import { getDatabase, ref, child, push, update } from "firebase/database";

function writeNewPost(uid, username, picture, title, body) {
  const db = getDatabase();

  // A post entry.
  const postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  const newPostKey = push(child(ref(db), 'posts')).key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  const updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return update(ref(db), updates);
}

Web

function writeNewPost(uid, username, picture, title, body) {
  // A post entry.
  var postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  var newPostKey = firebase.database().ref().child('posts').push().key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  var updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return firebase.database().ref().update(updates);
}

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

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

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

اگر می‌خواهید بدانید چه زمانی داده‌هایتان ثبت شده‌اند، می‌توانید یک تابع فراخوانی تکمیل (commission callback) اضافه کنید. هر دو set() و update() یک تابع فراخوانی تکمیل اختیاری می‌گیرند که وقتی نوشتن در پایگاه داده ثبت شده است، فراخوانی می‌شود. اگر فراخوانی ناموفق باشد، یک شیء خطا (error object) به تابع فراخوانی ارسال می‌شود که دلیل وقوع خطا را نشان می‌دهد.

Web

import { getDatabase, ref, set } from "firebase/database";

const db = getDatabase();
set(ref(db, 'users/' + userId), {
  username: name,
  email: email,
  profile_picture : imageUrl
})
.then(() => {
  // Data saved successfully!
})
.catch((error) => {
  // The write failed...
});

Web

firebase.database().ref('users/' + userId).set({
  username: name,
  email: email,
  profile_picture : imageUrl
}, (error) => {
  if (error) {
    // The write failed...
  } else {
    // Data saved successfully!
  }
});

حذف داده‌ها

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

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

دریافت Promise

برای اینکه بدانید داده‌هایتان چه زمانی به سرور Firebase Realtime Database ارسال می‌شوند، می‌توانید از یک Promise استفاده کنید. هم set() و update() می‌توانند یک Promise برگردانند که می‌توانید از آن برای اطلاع از زمان ارسال داده‌ها به پایگاه داده استفاده کنید.

جدا کردن شنوندگان

فراخوانی‌های برگشتی با فراخوانی متد off() در مرجع پایگاه داده Firebase شما حذف می‌شوند.

شما می‌توانید با ارسال یک شنونده به عنوان پارامتر به off() آن را حذف کنید. فراخوانی off() در مکانی که هیچ آرگومانی ندارد، تمام شنونده‌های موجود در آن مکان را حذف می‌کند.

فراخوانی تابع off() در یک شنونده‌ی والد، شنونده‌های ثبت‌شده در گره‌های فرزند آن را به‌طور خودکار حذف نمی‌کند؛ همچنین باید off() در هر شنونده‌ی فرزندی فراخوانی شود تا فراخوانی برگشتی حذف شود.

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

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

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

Web

import { getDatabase, ref, runTransaction } from "firebase/database";

function toggleStar(uid) {
  const db = getDatabase();
  const postRef = ref(db, '/posts/foo-bar-123');

  runTransaction(postRef, (post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

Web

function toggleStar(postRef, uid) {
  postRef.transaction((post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

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

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

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

Web

function addStar(uid, key) {
  import { getDatabase, increment, ref, update } from "firebase/database";
  const dbRef = ref(getDatabase());

  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = increment(1);
  update(dbRef, updates);
}

Web

function addStar(uid, key) {
  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  firebase.database().ref().update(updates);
}

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

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

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

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

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

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

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

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

مراحل بعدی