המדריך הזה עוסק ביסודות של IndexedDB API.
אנחנו משתמשים בספרייה IndexedDB Promised של ג'ייק ארצ'יבלד, שדומה מאוד ל-IndexedDB API, אבל משתמשת בהבטחות, שאפשר await כדי לקבל תחביר תמציתי יותר. כך ה-API פשוט יותר, אבל המבנה שלו נשמר.
מה זה IndexedDB?
IndexedDB היא מערכת אחסון NoSQL בקנה מידה גדול, שמאפשרת לאחסן כמעט כל דבר בדפדפן של המשתמש. בנוסף לפעולות הרגילות של חיפוש, אחזור והוספה, IndexedDB תומך גם בטרנזקציות, והוא מתאים במיוחד לאחסון כמויות גדולות של נתונים מובְנים.
כל מסד נתונים של IndexedDB הוא ייחודי למקור (בדרך כלל הדומיין או תת-הדומיין של האתר), כלומר הוא לא יכול לגשת למקור אחר או להיות נגיש על ידי מקור אחר. מגבלות אחסון הנתונים שלו בדרך כלל גדולות, אם הן קיימות בכלל, אבל דפדפנים שונים מטפלים במגבלות ובפינוי נתונים באופן שונה. למידע נוסף, עיינו בקטע מקורות מידע נוספים.
מונחים שקשורים ל-IndexedDB
- מסד נתונים
- הרמה הגבוהה ביותר של IndexedDB. הוא מכיל את מאגרי האובייקטים, שמכילים את הנתונים שרוצים לשמור. אפשר ליצור כמה מסדי נתונים עם שמות שונים.
- מאגר אובייקטים
- קטגוריה נפרדת לאחסון נתונים, בדומה לטבלאות במסדי נתונים רלציוניים.
בדרך כלל, יש מאגר אובייקטים אחד לכל סוג (לא סוג נתונים של JavaScript) של נתונים שאתם מאחסנים. בניגוד לטבלאות של מסדי נתונים, סוגי הנתונים של JavaScript במאגר לא צריכים להיות עקביים. לדוגמה, אם לאפליקציה יש מאגר אובייקטים מסוג
peopleשמכיל מידע על שלושה אנשים, מאפייני הגיל של האנשים האלה יכולים להיות53,'twenty-five'ו-unknown. - אינדקס
- סוג של מאגר אובייקטים לארגון נתונים במאגר אובייקטים אחר (שנקרא מאגר אובייקטים של קובצי עזר) לפי מאפיין ספציפי של הנתונים. האינדקס משמש לאחזור רשומות במאגר האובייקטים לפי המאפיין הזה. לדוגמה, אם אתם מאחסנים אנשים, יכול להיות שתרצו לאחזר אותם מאוחר יותר לפי השם, הגיל או החיה האהובה עליהם.
- פעולה
- אינטראקציה עם מסד הנתונים.
- עסקה
- מעטפת סביב פעולה או קבוצת פעולות שמבטיחה את תקינות מסד הנתונים. אם אחת מהפעולות בטרנזקציה נכשלת, אף אחת מהן לא מיושמת ומסד הנתונים חוזר למצב שבו היה לפני שהטרנזקציה התחילה. כל פעולות הקריאה או הכתיבה ב-IndexedDB חייבות להיות חלק מטרנזקציה. כך אפשר לבצע פעולות קריאה-שינוי-כתיבה אטומיות בלי סיכון להתנגשויות עם שרשורים אחרים שפועלים במסד הנתונים בו-זמנית.
- Cursor
- מנגנון לחזרה על מספר רשומות במסד נתונים.
איך בודקים אם יש תמיכה ב-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, אתם יכולים ליצור כמה מסדי נתונים עם שמות לבחירתכם. אם מסד הנתונים לא קיים כשמנסים לפתוח אותו, הוא נוצר באופן אוטומטי.
כדי לפתוח מסד נתונים, משתמשים ב-method 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();
השיטה הזו מחזירה הבטחה (promise) שמתייחסת לאובייקט של מסד נתונים. כשמשתמשים ב-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(). אובייקט האירועים חושף את ה-method upgrade() שמאפשרת ליצור מאגרי אובייקטים. מפעילים את ה-method 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();
איך מגדירים אינדקסים
אינדקסים הם סוג של מאגר אובייקטים שמשמש לאחזור נתונים ממאגר האובייקטים של קובץ העזר לפי מאפיין מסוים. אינדקס נמצא בתוך מאגר האובייקטים של קובץ העזר ומכיל את אותם נתונים, אבל משתמש במאפיין שצוין כנתיב המפתח שלו במקום במפתח הראשי של מאגר קובצי העזר. צריך ליצור אינדקסים כשיוצרים את מאגרי האובייקטים, ואפשר להשתמש בהם כדי להגדיר אילוץ ייחודי על הנתונים.
כדי ליצור אינדקס, צריך להפעיל את ה-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();
השיטה הזו יוצרת ומחזירה אובייקט אינדקס. ה-method 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().
איך לעבוד עם נתונים
בקטע הזה מוסבר איך ליצור, לקרוא, לעדכן ולמחוק נתונים. כל הפעולות האלה הן אסינכרוניות, ומשתמשות ב-promises במקומות שבהם IndexedDB API משתמש בבקשות. כך ה-API פשוט יותר. במקום להאזין לאירועים שמופעלים על ידי הבקשה, אפשר להפעיל את .then() באובייקט מסד הנתונים שמוחזר מהשיטה openDB() כדי להתחיל אינטראקציות עם מסד הנתונים, או await את היצירה שלו.
כל פעולות הנתונים ב-IndexedDB מתבצעות בתוך טרנזקציה. כל פעולה היא בפורמט הבא:
- אחזור אובייקט של מסד נתונים.
- פתיחת טרנזקציה במסד הנתונים.
- פתיחת מאגר אובייקטים בטרנזקציה.
- ביצוע פעולה במאגר האובייקטים.
אפשר לחשוב על טרנזקציה כמעטפת בטוחה סביב פעולה או קבוצת פעולות. אם אחת מהפעולות בטרנזקציה נכשלת, כל הפעולות מבוטלות. הטרנזקציות הן ספציפיות למאגר אובייקטים אחד או יותר, שאתם מגדירים כשאתם פותחים את הטרנזקציה. הם יכולים להיות לקריאה בלבד או לקריאה וכתיבה. הוא מציין אם הפעולות בתוך הטרנזקציה קוראות את הנתונים או מבצעות שינוי במסד הנתונים.
יצירת נתונים
כדי ליצור נתונים, צריך לבצע קריאה ל-method add() במכונה של מסד הנתונים ולהעביר את הנתונים שרוצים להוסיף. הארגומנט הראשון של השיטה add() הוא מאגר האובייקטים שאליו רוצים להוסיף את הנתונים, והארגומנט השני הוא אובייקט שמכיל את השדות ואת הנתונים המשויכים שרוצים להוסיף. הנה הדוגמה הפשוטה ביותר, שבה מוסיפים שורת נתונים אחת:
import {openDB} from 'idb';
async function addItemToStore () {
const db = await openDB('example-database', 1);
await db.add('storeName', {
field: 'data'
});
}
addItemToStore();
כל קריאה ל-add() מתרחשת בתוך טרנזקציה, כך שגם אם ה-promise נפתר בהצלחה, זה לא בהכרח אומר שהפעולה הצליחה. כדי לוודא שפעולת ההוספה בוצעה, צריך לבדוק אם כל הטרנזקציה הושלמה באמצעות השיטה transaction.done(). זהו אובייקט promise שמותאם כשהעסקה מסתיימת, ונדחה אם יש שגיאה בעסקה. אתם חייבים לבצע את הבדיקה הזו בכל פעולות ה"כתיבה", כי זו הדרך היחידה לדעת שהשינויים במסד הנתונים אכן בוצעו.
הקוד הבא מראה איך להשתמש ב-method 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', וכל אחת מהן מחזירה הבטחה:
- מוסיפים רשומה של כריך טעים.
- מוסיף רשומה של ביצים.
- העסקה הושלמה (
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() בהבטחה.
בדוגמה הבאה נעשה שימוש ב-method 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();
אחזור של שורה אחת ממסד הנתונים הוא די פשוט: פותחים את מסד הנתונים ומציינים את מאגר האובייקטים ואת ערך המפתח הראשי של השורה שרוצים לאחזר ממנה נתונים. השיטה get() מחזירה הבטחה, אז אפשר להשתמש ב-await.
עדכון נתונים
כדי לעדכן נתונים, מפעילים את ה-method 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 כמפתח הראשי.
מחיקת נתונים
כדי למחוק נתונים, מפעילים את ה-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() או סמנים.
ה-method 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().bound()(שכולל את שניהם).only().includes().
ה-methods 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() מחזירה סמן שמייצג את האובייקט הבא, וממשיכה בלולאה עד שמגיעים לסוף הטווח.
ניהול גרסאות של מסדי נתונים
כשמפעילים את השיטה openDB(), אפשר לציין את מספר הגרסה של מסד הנתונים בפרמטר השני. בכל הדוגמאות במדריך הזה, הגרסה הוגדרה ל-1, אבל אפשר לשדרג מסד נתונים לגרסה חדשה אם צריך לשנות אותו בדרך כלשהי. אם הגרסה שצוינה גדולה מהגרסה של מסד הנתונים הקיים, הקריאה החוזרת (callback) של upgrade באובייקט האירוע מופעלת, ומאפשרת להוסיף מאגרי אובייקטים ואינדקסים חדשים למסד הנתונים.
לאובייקט db בקריאה החוזרת (callback) של 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.