עבודה עם IndexedDB

במדריך הזה מפורטים העקרונות הבסיסיים של IndexedDB API. אנחנו משתמשים בספרייה IndexedDB Promised של ג'ייק ארצ'יבלד, שדומה מאוד ל-IndexedDB API, אבל נעשה בה שימוש בהבטחות, ואפשר await לתחביר תמציתי יותר. כך ה-API קל יותר והמבנה שלו נשמר.

מה זה IndexedDB?

IndexedDB היא מערכת אחסון גדולה מסוג NoSQL, שמאפשרת לאחסן כמעט כל דבר בדפדפן של המשתמש. בנוסף לפעולות החיפוש, הקבלה וההנחיה הרגילות, IndexedDB תומך גם בטרנזקציות, ומתאים לאחסון כמויות גדולות של נתונים מובְנים.

כל מסד נתונים IndexedDB הוא ייחודי למקור (בדרך כלל הדומיין או תת-הדומיין של האתר), כלומר לא ניתן לגשת אליו או לגשת אליו מכל מקור אחר. מגבלות אחסון הנתונים שלו בדרך כלל גדולות, אם קיימות בכלל, אבל דפדפנים שונים מטפלים במגבלות ובהסרת הנתונים באופן שונה. למידע נוסף, קראו את הקטע קריאה נוספת.

תנאים ב-IndexedDB

מסד נתונים
הרמה הגבוהה ביותר של IndexedDB. הקובץ מכיל את מאגרי האובייקטים, שכתוצאה מכך מכילים את הנתונים שרוצים שיישמרו. אפשר ליצור כמה מסדי נתונים בכל שם שרוצים.
אחסון אובייקטים
קטגוריה בודדת לאחסון נתונים, בדומה לטבלאות במסדי נתונים רלציוניים. בדרך כלל יש אחסון אובייקטים אחד לכל סוג (לא סוג נתונים של JavaScript) של נתונים שאתם שומרים. בניגוד לטבלאות של מסדי נתונים, סוגי הנתונים של JavaScript בחנות לא חייבים להיות עקביים. לדוגמה, אם לאפליקציה יש מאגר אובייקטים people שמכיל מידע על שלושה אנשים, מאפייני הגיל של האנשים האלה יכולים להיות 53, 'twenty-five' ו-unknown.
אינדקס
סוג של אחסון אובייקטים לארגון נתונים במאגר אובייקטים אחר (שנקרא מאגר אובייקט הפניה) לפי מאפיין בודד של הנתונים. האינדקס משמש לאחזור רשומות במאגר האובייקטים על ידי הנכס הזה. לדוגמה, אם אתם שומרים אנשים, כדאי לכם לאחזר אותם מאוחר יותר לפי השם, הגיל או בעל החיים המועדף עליהם.
פעולה
אינטראקציה עם מסד הנתונים.
עסקה
wrapper מסביב לפעולה או קבוצה של פעולות שמבטיחה את תקינות מסד הנתונים. אם אחת מהפעולות בטרנזקציה נכשלת, אף אחת מהן לא מיושמת ומסד הנתונים חוזר למצב שבו היה לפני שהעסקה התחילה. כל פעולות הקריאה או הכתיבה ב-IndexedDB חייבות להיות חלק מעסקה. כך אפשר לבצע פעולות קריאה-שינוי-כתיבה אטומיות בלי להסתכן בהתנגשויות עם שרשורים אחרים שפועלים במסד הנתונים בו-זמנית.
סמן
מנגנון לאיטרציה על כמה רשומות במסד נתונים.

איך בודקים אם יש תמיכה ב-IndexedDB

IndexedDB נתמך באופן כמעט אוניברסלי. עם זאת, אם אתם עובדים עם דפדפנים ישנים, לא כדאי גם להשתמש בתמיכה לזיהוי תכונות ליתר ביטחון. הדרך הקלה ביותר היא לבדוק את האובייקט window:

function indexedDBStuff () {
  // Check for IndexedDB support:
  if (!('indexedDB' in window)) {
    // Can't use IndexedDB
    console.log("This browser doesn't support IndexedDB");
    return;
  } else {
    // Do IndexedDB stuff here:
    // ...
  }
}

// Run IndexedDB code:
indexedDBStuff();

פתיחת מסד נתונים

באמצעות IndexedDB תוכל ליצור מסדי נתונים מרובים בכל שם שתבחר. אם לא קיים מסד נתונים כשמנסים לפתוח אותו, הוא נוצר באופן אוטומטי. כדי לפתוח מסד נתונים, יש להשתמש בשיטה openDB() מהספרייה idb:

import {openDB} from 'idb';

async function useDB () {
  // Returns a promise, which makes `idb` usable with async-await.
  const dbPromise = await openDB('example-database', version, events);
}

useDB();

השיטה הזו מחזירה הבטחה שמובילה לאובייקט מסד נתונים. כשמשתמשים בשיטה openDB(), צריך לציין שם, מספר גרסה ואובייקט אירועים כדי להגדיר את מסד הנתונים.

הנה דוגמה לשיטה openDB() בהקשר:

import {openDB} from 'idb';

async function useDB () {
  // Opens the first version of the 'test-db1' database.
  // If the database does not exist, it will be created.
  const dbPromise = await openDB('test-db1', 1);
}

useDB();

בודקים אם יש תמיכה ב-IndexedDB בחלק העליון של הפונקציה האנונימית. הפעולה הזו יוצאת מהפונקציה אם הדפדפן לא תומך ב-IndexedDB. אם הפונקציה יכולה להמשיך, היא קוראת ל-method openDB() לפתוח מסד נתונים בשם 'test-db1'. בדוגמה הזו, אובייקט האירועים האופציונליים נשאר ריק כדי לשמור על פשטות, אבל צריך לציין אותו כדי לבצע עבודה משמעותית עם IndexedDB.

איך לעבוד עם מאגרי אובייקטים

מסד נתונים IndexedDB מכיל מאגר אובייקטים אחד או יותר, שבכל אחת מהן יש עמודה למפתח, ועמודה נוספת לנתונים המשויכים למפתח הזה.

יצירת מאגרי אובייקטים

במסד נתונים של IndexedDB מובנה היטב צריך להיות אחסון אובייקטים אחד לכל סוג נתונים שצריך לשמור. לדוגמה, באתר ששומר פרופילים של משתמשים והערות עשוי להיות אחסון אובייקטים people שמכיל אובייקטים person, ומאגר אובייקטים notes שמכיל note אובייקטים.

כדי להבטיח את תקינות מסד הנתונים, אפשר ליצור או להסיר מאגרי אובייקטים רק באובייקט האירועים בקריאה ל-openDB(). אובייקט האירועים חושף את השיטה upgrade() שמאפשרת ליצור מאגרי אובייקטים. כדי ליצור את אחסון האובייקטים, צריך לקרוא ל-method createObjectStore() בתוך השיטה upgrade():

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('example-database', 1, {
    upgrade (db) {
      // Creates an object store:
      db.createObjectStore('storeName', options);
    }
  });
}

createStoreInDB();

השיטה הזו מקבלת את השם של אחסון האובייקטים, ואובייקט תצורה אופציונלי שמאפשר להגדיר מאפיינים שונים לאחסון האובייקטים.

הדוגמה הבאה לשימוש ב-createObjectStore():

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db1', 1, {
    upgrade (db) {
      console.log('Creating a new object store...');

      // Checks if the object store exists:
      if (!db.objectStoreNames.contains('people')) {
        // If the object store does not exist, create it:
        db.createObjectStore('people');
      }
    }
  });
}

createStoreInDB();

בדוגמה הזו, אובייקט אירועים מועבר לשיטה openDB() כדי ליצור את אחסון האובייקט, וכמו קודם, העבודה על יצירת אחסון האובייקט מתבצעת בשיטת upgrade() של אובייקט האירוע. עם זאת, מכיוון שהדפדפן יוצר הודעת שגיאה כשמנסים ליצור מאגר אובייקטים שכבר קיים, מומלץ לכלול את השיטה createObjectStore() בהצהרה if שבודקת אם אחסון האובייקטים קיים. בתוך הבלוק if, צריך לקרוא ל-createObjectStore() כדי ליצור אחסון אובייקטים בשם 'firstOS'.

איך מגדירים מפתחות ראשיים

כשמגדירים אחסון אובייקטים, אפשר להגדיר איך נתונים מזוהים באופן ייחודי בחנות באמצעות מפתח ראשי. אפשר להגדיר מפתח ראשי באמצעות הגדרת נתיב של מפתח, או באמצעות מחולל מפתחות.

נתיב מפתח הוא נכס שקיים תמיד ומכיל ערך ייחודי. לדוגמה, במקרה של מאגר אובייקטים people, תוכלו לבחור בכתובת האימייל כנתיב המפתח:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }
    }
  });
}

createStoreInDB();

בדוגמה הזו נוצר מאגר אובייקטים בשם 'people', ומקצה את המאפיין email כמפתח הראשי באפשרות keyPath.

אפשר גם להשתמש במחולל מפתחות כמו autoIncrement. מחולל המפתחות יוצר ערך ייחודי לכל אובייקט שמתווסף למאגר האובייקטים. כברירת מחדל, אם לא מציינים מפתח, IndexedDB יוצר מפתח ושומר אותו בנפרד מהנתונים.

הדוגמה הבאה יוצרת מאגר אובייקטים בשם 'notes' ומגדירה את המפתח הראשי כך שיוקצה באופן אוטומטי כמספר עולה באופן אוטומטי:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

הדוגמה הבאה דומה לדוגמה הקודמת, אבל הפעם הערך של ההגדלה האוטומטית מוקצה במפורש למאפיין בשם 'id'.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

הבחירה באיזו שיטה להגדיר את המפתח תלויה בנתונים שלכם. אם לנתונים שלכם יש מאפיין תמיד ייחודי, אפשר להגדיר אותם בתור keyPath כדי לאכוף את הייחודיות הזו. אחרת, אפשר להשתמש בערך בהגדלה אוטומטית.

הקוד הבא יוצר שלושה מאגרי אובייקטים שמדגימים את הדרכים השונות להגדרת מפתחות ראשיים במאגרי אובייקטים:

import {openDB} from 'idb';

async function createStoresInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }

      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }

      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoresInDB();

איך מגדירים אינדקסים

אינדקסים הם סוג של אחסון אובייקטים שמשמש לאחזור נתונים ממאגר האובייקטים של ההפניה על ידי מאפיין ספציפי. אינדקס נמצא בתוך מאגר אובייקט ההפניה ומכיל את אותם נתונים, אבל הוא משתמש במאפיין שצוין כנתיב המפתח שלו במקום במפתח הראשי של מאגר העזר. צריך ליצור אינדקסים כשיוצרים את מאגרי האובייקטים, ואפשר להשתמש בהם כדי להגדיר מגבלה ייחודית על הנתונים.

על מנת ליצור אינדקס, צריך לקרוא ל-method createIndex() במכונה של אחסון אובייקטים:

import {openDB} from 'idb';

async function createIndexInStore() {
  const dbPromise = await openDB('storeName', 1, {
    upgrade (db) {
      const objectStore = db.createObjectStore('storeName');

      objectStore.createIndex('indexName', 'property', options);
    }
  });
}

createIndexInStore();

השיטה הזו יוצרת ומחזירה אובייקט אינדקס. השיטה createIndex() במכונה של מאגר האובייקטים מקבלת את שם האינדקס החדש כארגומנט הראשון, והארגומנט השני מתייחס למאפיין על הנתונים שרוצים ליצור להם אינדקס. הארגומנט האחרון מאפשר להגדיר שתי אפשרויות שקובעות את אופן הפעולה של האינדקס: unique ו-multiEntry. אם הערך של unique מוגדר ל-true, האינדקס לא מאפשר ערכים כפולים למפתח יחיד. בשלב הבא, multiEntry קובע איך createIndex() יפעל כשהנכס שנוסף לאינדקס הוא מערך. אם היא מוגדרת לערך true, createIndex() מוסיפה רשומה לאינדקס לכל רכיב מערך. אחרת, היא מוסיפה רשומה אחת שמכילה את המערך.

לדוגמה:

import {openDB} from 'idb';

async function createIndexesInStores () {
  const dbPromise = await openDB('test-db3', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        const peopleObjectStore = db.createObjectStore('people', { keyPath: 'email' });

        peopleObjectStore.createIndex('gender', 'gender', { unique: false });
        peopleObjectStore.createIndex('ssn', 'ssn', { unique: true });
      }

      if (!db.objectStoreNames.contains('notes')) {
        const notesObjectStore = db.createObjectStore('notes', { autoIncrement: true });

        notesObjectStore.createIndex('title', 'title', { unique: false });
      }

      if (!db.objectStoreNames.contains('logs')) {
        const logsObjectStore = db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createIndexesInStores();

בדוגמה הזו, למאגרי האובייקטים 'people' ו-'notes' יש אינדקסים. כדי ליצור את האינדקסים, קודם צריך להקצות למשתנה את התוצאה של createObjectStore() (אובייקט אחסון של אובייקט) כדי לקרוא לו createIndex().

איך לעבוד עם נתונים

בקטע הזה מוסבר איך ליצור, לקרוא, לעדכן ולמחוק נתונים. כל הפעולות האלה הן אסינכרוניות, ונעשה בהן שימוש בהבטחות שבהן ה-API של IndexedDB משתמש בבקשות. זה מפשט את ה-API. במקום להקשיב לאירועים שהופעלו על ידי הבקשה, אפשר לקרוא לפונקציה .then() באובייקט מסד הנתונים שהוחזר מהשיטה openDB() כדי להתחיל אינטראקציות עם מסד הנתונים או כדי ליצור אותו await.

כל פעולות הנתונים ב-IndexedDB מתבצעות בתוך עסקה. לכל פעולה יש את הצורה הבאה:

  1. אחזור של אובייקט מסד נתונים.
  2. פתיחת הטרנזקציה במסד הנתונים.
  3. פתיחת אחסון אובייקטים בטרנזקציה.
  4. ביצוע פעולה על אחסון אובייקטים.

אפשר להתייחס לעסקה כאריזה בטוחה מסביב לפעולה או לקבוצת פעולות. אם אחת מהפעולות בתוך העסקה נכשלת, כל הפעולות מוחזרות. טרנזקציות הן ספציפיות למאגר אובייקטים אחד או יותר, שמגדירים כשפותחים את הטרנזקציה. הם יכולים להיות לקריאה בלבד או לקריאה וכתיבה. מציין אם הפעולות בתוך הטרנזקציה קוראות את הנתונים או מבצעות שינוי במסד הנתונים.

יצירת נתונים

כדי ליצור נתונים, צריך לקרוא ל-method add() במופע של מסד הנתונים ולהעביר את הנתונים שרוצים להוסיף. הארגומנט הראשון ב-method add() הוא מאגר האובייקטים שאליו רוצים להוסיף את הנתונים, והארגומנט השני הוא אובייקט שמכיל את השדות והנתונים המשויכים שרוצים להוסיף. הנה הדוגמה הפשוטה ביותר, שבה מתווספת שורת נתונים יחידה:

import {openDB} from 'idb';

async function addItemToStore () {
  const db = await openDB('example-database', 1);

  await db.add('storeName', {
    field: 'data'
  });
}

addItemToStore();

כל קריאה ל-add() מתבצעת במסגרת עסקה, כך שגם אם ההבטחה נפתרה בהצלחה, זה לא בהכרח אומר שהפעולה הצליחה. כדי לוודא שפעולת ההוספה בוצעה, צריך לבדוק אם העסקה כולה הושלמה באמצעות השיטה transaction.done(). זוהי הבטחה שנגרמת כשהיא תושלם כשהעסקה תושלם, ותדחה אם השגיאות יופיעו. עליכם לבצע את הבדיקה הזו על כל פעולות 'כתיבה', מכיוון שזו הדרך היחידה לדעת שהשינויים במסד הנתונים התרחשו בפועל.

הקוד הבא מציג את השימוש בשיטה add() בתוך עסקה:

import {openDB} from 'idb';

async function addItemsToStore () {
  const db = await openDB('test-db4', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('foods')) {
        db.createObjectStore('foods', { keyPath: 'name' });
      }
    }
  });
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Add multiple items to the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.add({
      name: 'Sandwich',
      price: 4.99,
      description: 'A very tasty sandwich!',
      created: new Date().getTime(),
    }),
    tx.store.add({
      name: 'Eggs',
      price: 2.99,
      description: 'Some nice eggs you can cook up!',
      created: new Date().getTime(),
    }),
    tx.done
  ]);
}

addItemsToStore();

אחרי שפותחים את מסד הנתונים (ויוצרים מאגר אובייקטים במקרה הצורך), תצטרכו לפתוח טרנזקציה על ידי קריאה לשיטה transaction() שבה. השיטה הזו לוקחת בחשבון את הארגומנט של החנות שבה רוצים לבצע עסקאות וגם את המצב. במקרה זה, אנחנו רוצים לכתוב לחנות, ולכן בדוגמה הזו מצוין 'readwrite'.

השלב הבא הוא הוספת פריטים לחנות כחלק מהעסקה. בדוגמה שלמעלה מדובר בשלוש פעולות בחנות 'foods', שכל אחת מהן מניבה הבטחה:

  1. רוצה להוסיף תיעוד של כריך טעים?
  2. מוסיפים רשומה לכמה ביצים.
  3. סימן שהעסקה הושלמה (tx.done).

מכיוון שכל הפעולות האלה מבוססות על הבטחות, אנחנו צריכים לחכות שכולן יסתיימו. העברת ההבטחות האלה ל-Promise.all היא דרך ארגונומית ונחמדה לעשות זאת. Promise.all מקבל מערך של הבטחות ומסתיים לאחר שכל ההבטחות שהועברו אליו טופלו.

לשתי הרשומות שמתווספות, ממשק store של המופע קורא ל-add() ומעביר את הנתונים אליו. ניתן await את הקריאה ל-Promise.all כדי שהיא תסתיים כשהעסקה תושלם.

קריאת נתונים

כדי לקרוא נתונים, צריך לקרוא לשיטה get() במופע מסד הנתונים שמאחזרים באמצעות השיטה openDB(). get() מקבל את שם המאגר ואת ערך המפתח הראשי של האובייקט שרוצים לאחזר. הנה דוגמה בסיסית:

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('example-database', 1);

  // Get a value from the object store by its primary key value:
  const value = await db.get('storeName', 'unique-primary-key-value');
}

getItemFromStore();

כמו ב-add(), השיטה get() מחזירה הבטחה, כך שתוכלו await אותה אם תרצו, או להשתמש בקריאה חוזרת (callback) של .then() של ההבטחה.

בדוגמה הבאה נשתמש בשיטה get() במאגר האובייקטים 'foods' במסד הנתונים 'test-db4', כדי לקבל שורה אחת לפי המפתח הראשי של 'name':

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('test-db4', 1);
  const value = await db.get('foods', 'Sandwich');

  console.dir(value);
}

getItemFromStore();

קל לאחזר שורה אחת ממסד הנתונים: פותחים את מסד הנתונים ומציינים את מאגר האובייקטים ואת ערך המפתח הראשי של השורה שממנה רוצים לקבל נתונים. מכיוון שה-method get() מחזירה הבטחה, אפשר await אותה.

עדכון נתונים

כדי לעדכן את הנתונים, צריך לקרוא ל-method put() באחסון האובייקטים. השיטה put() דומה ל-method add() ואפשר גם להשתמש בה במקום add() כדי ליצור נתונים. דוגמה בסיסית לשימוש ב-put() כדי לעדכן שורה במאגר אובייקטים לפי ערך המפתח הראשי שלה:

import {openDB} from 'idb';

async function updateItemInStore () {
  const db = await openDB('example-database', 1);

  // Update a value from in an object store with an inline key:
  await db.put('storeName', { inlineKeyName: 'newValue' });

  // Update a value from in an object store with an out-of-line key.
  // In this case, the out-of-line key value is 1, which is the
  // auto-incremented value.
  await db.put('otherStoreName', { field: 'value' }, 1);
}

updateItemInStore();

כמו בשיטות אחרות, השיטה הזו מחזירה הבטחה. אפשר להשתמש ב-put() גם כחלק מעסקה. הנה דוגמה לשימוש בחנות 'foods' מקודם שמעדכנת את מחיר הכריך והביצים:

import {openDB} from 'idb';

async function updateItemsInStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Update multiple items in the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.put({
      name: 'Sandwich',
      price: 5.99,
      description: 'A MORE tasty sandwich!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.store.put({
      name: 'Eggs',
      price: 3.99,
      description: 'Some even NICER eggs you can cook up!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.done
  ]);
}

updateItemsInStore();

אופן העדכון של הפריטים תלוי באופן שבו מגדירים את המפתח. אם מגדירים keyPath, כל שורה במאגר האובייקטים משויכת למפתח מוטבע. הדוגמה הקודמת מעדכנת את השורות על סמך המפתח הזה, וכשמעדכנים שורות במצב הזה, צריך לציין את המפתח כדי לעדכן את הפריט המתאים במאגר האובייקטים. אפשר גם ליצור מפתח מחוץ לשורה על ידי הגדרה של autoIncrement כמפתח הראשי.

מחיקת נתונים

כדי למחוק נתונים, צריך לקרוא ל-method delete() באחסון האובייקטים:

import {openDB} from 'idb';

async function deleteItemFromStore () {
  const db = await openDB('example-database', 1);

  // Delete a value 
  await db.delete('storeName', 'primary-key-value');
}

deleteItemFromStore();

כמו add() ו-put(), ניתן להשתמש בזה כחלק מעסקה:

import {openDB} from 'idb';

async function deleteItemsFromStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Delete multiple items from the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.delete('Sandwich'),
    tx.store.delete('Eggs'),
    tx.done
  ]);
}

deleteItemsFromStore();

המבנה של האינטראקציה במסד הנתונים זהה לזה של הפעולות האחרות. חשוב לבדוק שכל העסקה הושלמה על ידי הוספת השיטה tx.done במערך שמעבירים ל-Promise.all.

המערכת מקבלת את כל הנתונים

עד עכשיו אחזרת אובייקטים מהחנות, אחד בכל פעם. אפשר גם לאחזר את כל הנתונים, או קבוצת משנה, ממאגר או מאינדקס של אובייקטים באמצעות השיטה getAll() או הסמנים.

שיטת getAll()

הדרך הפשוטה ביותר לאחזר את כל הנתונים של אחסון אובייקטים היא לקרוא ל-getAll() באחסון או באינדקס האובייקטים, כך:

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('storeName');

  console.dir(allValues);
}

getAllItemsFromStore();

השיטה הזו מחזירה את כל האובייקטים במאגר האובייקטים, ללא אילוצים בכלל. זו הדרך הישירה ביותר לקבל את כל הערכים ממאגר אובייקטים, אבל גם הכי פחות גמישה.

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('foods');

  console.dir(allValues);
}

getAllItemsFromStore();

בדוגמה הזו מתבצעת קריאה ל-getAll() באחסון האובייקטים 'foods'. הפעולה הזו מחזירה את כל האובייקטים מ-'foods', מסודרים לפי המפתח הראשי.

איך להשתמש בסמנים

הסמןים הם דרך גמישה יותר לאחזר אובייקטים מרובים. סמן בוחר כל אובייקט במאגר או אינדקס אובייקטים בזה אחר זה, ומאפשר לך לעשות משהו עם הנתונים כאשר הם נבחרים. הסמןים, כמו פעולות אחרות במסד הנתונים, פועלים בטרנזקציות.

כדי ליצור סמן, צריך להפעיל את openCursor() במאגר האובייקטים כחלק מעסקה. באמצעות האחסון 'foods' מהדוגמאות הקודמות, כך מעבירים סמן בין כל שורות הנתונים במאגר אובייקטים:

import {openDB} from 'idb';

async function getAllItemsFromStoreWithCursor () {
  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');

  // Open a cursor on the designated object store:
  let cursor = await tx.store.openCursor();

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

getAllItemsFromStoreWithCursor();

במקרה הזה העסקה נפתחת במצב 'readonly', וה-method openCursor נקראת. בלולאת while הבאה, השורה במיקום הנוכחי של הסמן יכולה לקרוא את המאפיינים key ו-value, ותוכלו לפעול על הערכים האלה בכל דרך שמתאימה לאפליקציה שלכם. כשתהיו מוכנים, תוכלו להפעיל את השיטה continue() של האובייקט cursor כדי לעבור לשורה הבאה, והלולאת while מסתיימת כשהסמן מגיע לסוף מערך הנתונים.

שימוש בסמנים עם טווחים ואינדקסים

אינדקסים מאפשרים לאחזר את הנתונים במאגר אובייקטים לפי מאפיין שהוא לא המפתח הראשי. אפשר ליצור אינדקס בכל נכס, שהופך ל-keyPath של האינדקס, לציין טווח בנכס הזה ולקבל את הנתונים בטווח באמצעות getAll() או סמן.

מגדירים את הטווח באמצעות האובייקט IDBKeyRange, באחת מהשיטות הבאות:

השיטות upperBound() ו-lowerBound() מציינות את הגבול העליון והתחתון של הטווח.

IDBKeyRange.lowerBound(indexKey);

או:

IDBKeyRange.upperBound(indexKey);

כל אחד מהם משתמש בארגומנט אחד: ערך keyPath של האינדקס עבור הפריט שרוצים להגדיר כמגבלה העליונה או התחתונה.

השיטה bound() מציינת גבול עליון וגם גבול תחתון:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

כברירת מחדל, הטווח של הפונקציות האלה כולל את כל הנתונים, כלומר הוא כולל את הנתונים שצוינו כמגבלות של הטווח. כדי לא לכלול את הערכים האלה, צריך לציין את הטווח כבלעדי על ידי העברת true כארגומנט השני של lowerBound() או של upperBound(), או כארגומנטים השלישי והרביעי של bound(), לגבול התחתון והעליון, בהתאמה.

בדוגמה הבאה נעשה שימוש באינדקס של המאפיין 'price' במאגר האובייקטים 'foods'. לחנות יש עכשיו גם טופס עם שני מקורות קלט עבור הגבול העליון והתחתון של הטווח. השתמשו בקוד הבא כדי למצוא מוצרי מזון שהמחירים שלהם נעים בין המגבלות האלה:

import {openDB} from 'idb';

async function searchItems (lower, upper) {
  if (!lower === '' && upper === '') {
    return;
  }

  let range;

  if (lower !== '' && upper !== '') {
    range = IDBKeyRange.bound(lower, upper);
  } else if (lower === '') {
    range = IDBKeyRange.upperBound(upper);
  } else {
    range = IDBKeyRange.lowerBound(lower);
  }

  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');
  const index = tx.store.index('price');

  // Open a cursor on the designated object store:
  let cursor = await index.openCursor(range);

  if (!cursor) {
    return;
  }

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

// Get items priced between one and four dollars:
searchItems(1.00, 4.00);

הקוד לדוגמה מקבל קודם את הערכים של הגבולות ובודק אם הן קיימות. קטע הקוד הבא קובע באיזו שיטה להגביל את הטווח בהתאם לערכים. באינטראקציה של מסד הנתונים, פותחים כרגיל את אחסון האובייקטים בטרנזקציה, ואז פותחים את האינדקס 'price' במאגר האובייקטים. האינדקס 'price' מאפשר לחפש פריטים לפי מחיר.

לאחר מכן הקוד פותח את הסמן על האינדקס ועובר בטווח. הסמן מחזיר הבטחה שמייצגת את האובייקט הראשון בטווח, או undefined אם אין נתונים בטווח. השיטה cursor.continue() מחזירה סמן שמייצג את האובייקט הבא, וממשיכה לאורך הלולאה עד שמגיעים לסוף הטווח.

ניהול גרסאות של מסדי נתונים

כשמבצעים קריאה ל-method openDB(), אפשר לציין את מספר הגרסה של מסד הנתונים בפרמטר השני. בכל הדוגמאות במדריך הזה, הגרסה הוגדרה כ-1, אבל אפשר לשדרג מסד נתונים לגרסה חדשה אם צריך לשנות אותו בצורה כלשהי. אם הגרסה שצוינה גדולה יותר מהגרסה של מסד הנתונים הקיים, תתבצע הקריאה החוזרת upgrade באובייקט האירוע, וכך תוכלו להוסיף למסד הנתונים מאגרים ואינדקסים חדשים של אובייקטים.

לאובייקט db בקריאה החוזרת של upgrade יש מאפיין oldVersion מיוחד, שמציין את מספר הגרסה של מסד הנתונים שלדפדפן יש גישה אליו. אפשר להעביר את מספר הגרסה הזה להצהרת switch על מנת להוציא לפועל בלוקים של קוד בתוך הקריאה החוזרת (callback) של upgrade על סמך מספר הגרסה הקיימת של מסד הנתונים. לדוגמה:

import {openDB} from 'idb';

const db = await openDB('example-database', 2, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');
    }
  }
});

בדוגמה הזו, הגרסה החדשה ביותר של מסד הנתונים מוגדרת ל-2. כשהקוד הזה מופעל בפעם הראשונה, מסד הנתונים עדיין לא קיים בדפדפן, ולכן oldVersion הוא 0 וההצהרה switch מתחילה ב-case 0. בדוגמה, מוסיפים למסד הנתונים אחסון של אובייקטים 'store'.

נקודה חשובה: בהצהרות switch, בדרך כלל יש break אחרי כל בלוק case, אבל לא משתמשים בו כאן בכוונה. כך, אם מסד הנתונים הקיים נמצא מאחורי כמה גרסאות או אם הוא לא קיים, הקוד ממשיך לשאר הבלוקים של case עד שהוא מעודכן. כך שבדוגמה, הדפדפן ממשיך לפעול דרך case 1 ויוצר אינדקס name באחסון האובייקטים store.

על מנת ליצור אינדקס 'description' במאגר האובייקטים 'store' צריך לעדכן את מספר הגרסה ולהוסיף בלוק case חדש באופן הבא:

import {openDB} from 'idb';

const db = await openDB('example-database', 3, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');

      case 2:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('description', 'description');
    }
  }
});

אם מסד הנתונים שיצרתם בדוגמה הקודמת עדיין קיים בדפדפן, כשהפעולה הזו מתבצעת, oldVersion הוא 2. הדפדפן מדלג על case 0 ו-case 1 ומפעיל את הקוד ב-case 2, וכך נוצר אינדקס description. לאחר מכן, בגרסה 3 לדפדפן יש מסד נתונים שמכיל מאגר אובייקטים store עם האינדקסים name ו-description.

קריאה נוספת

במקורות המידע הבאים אפשר למצוא עוד מידע והקשר לשימוש ב-IndexedDB.

מסמכי תיעוד ב-IndexedDB

מגבלות על אחסון נתונים