עבודה עם IndexedDB

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

מה זה IndexedDB?

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

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

תנאים של IndexedDB

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

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

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

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

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

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

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

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

כדי לשמור על תקינות מסד הנתונים, ניתן ליצור או להסיר מאגרי אובייקטים רק אובייקט של אירועים בקריאה ל-openDB(). אובייקט ה-events חושף upgrade() שיטה שמאפשרת ליצור מאגרי אובייקטים. קוראים לפונקציה createObjectStore() בתוך ה-method 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();

בדוגמה הזו, אובייקט אירועים מועבר ל-method openDB() כדי ליצור מאגר האובייקטים, וכמו קודם, היצירה של מאגר האובייקטים הושלמה ב-method 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();

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

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

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

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() (אובייקט store object) למשתנה, כדי שאפשר יהיה לקרוא למשתנה createIndex().

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

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

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

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

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

יצירת נתונים

כדי ליצור נתונים, קוראים לadd() במכונה של מסד הנתונים, ומעבירים את הנתונים שרוצים להוסיף. 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();

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

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

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

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

לגבי שתי הרשומות שנוספות, ממשק store של מופע העסקה קוראת לפונקציה add() ומעבירה את הנתונים אליה. אפשר await את השיחה של Promise.all כך שהיא תסתיים כשהעסקה תושלם.

קריאת נתונים

כדי לקרוא נתונים, צריך להפעיל את get() במכונת מסד הנתונים שמאחזרים באמצעות ה-method 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().

הדוגמה הבאה משתמשת ב-method get() במסד הנתונים 'test-db4' אחסון אובייקטים מסוג 'foods' כדי לקבל שורה אחת לפני המפתח הראשי '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();

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

עדכון נתונים

כדי לעדכן נתונים, צריך להפעיל את put() במאגר האובייקטים. השיטה put() דומה לשיטה 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 כמפתח הראשי.

מחיקת נתונים

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

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();

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

איסוף כל הנתונים

עד עכשיו אחזרת מהחנות רק אובייקטים אחד בכל פעם. אפשר גם מאחזרים את כל הנתונים, או קבוצת משנה, ממאגר או מאינדקס של אובייקטים באמצעות באמצעות ה-method 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', בוצעה קריאה לשיטה 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 אם אין נתונים בטווח. ה-method cursor.continue() מחזירה הסמן שמייצג את האובייקט הבא וממשיך בלולאה עד מגיע לסוף הטווח.

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

כשקוראים ל-method openDB(), אפשר לציין את מספר הגרסה של מסד הנתונים בפרמטר השני. בכל הדוגמאות במדריך זה, הגרסה מוגדרת לערך 1, אבל אפשר לשדרג מסד נתונים לגרסה חדשה אם צריך לשנות אותו באופן מסוים. אם הגרסה שצוינה היא הגרסה של במסד הנתונים הקיים, הקריאה החוזרת (callback) של 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

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