العمل باستخدام IndexedDB

يتناول هذا الدليل أساسيات IndexedDB API. نحن نستخدم مكتبة "IndexedDB Promized" لـ "جيك أرشيبالد"، والتي تشبه إلى حدّ كبير واجهة برمجة التطبيقات IndexedDB API، غير أنّها تستخدم الوعود التي يمكنك await استخدامها للحصول على بنية أكثر إيجازًا. وهذا يبسط واجهة برمجة التطبيقات مع الحفاظ على هيكلها.

ما هي IndexedDB؟

IndexedDB هو نظام تخزين NoSQL واسع النطاق يسمح بتخزين أي شيء تقريبًا في متصفّح المستخدم. إلى جانب إجراءات البحث والتلقي والوضع المعتادة، تتيح IndexedDB أيضًا إجراء المعاملات، وهي مناسبة تمامًا لتخزين كميات كبيرة من البيانات المنظَّمة.

إنّ كل قاعدة بيانات في IndexedDB تكون فريدة من نوعها لمصدر (عادةً نطاق الموقع الإلكتروني أو نطاقه الفرعي)، ما يعني أنّه لا يمكن الوصول إليها أو الوصول إليها من أي مصدر آخر. وعادةً ما تكون حدود تخزين البيانات كبيرة، إذا كانت موجودة من الأساس، ولكن المتصفحات المختلفة تتعامل مع الحدود وإخلاء البيانات بشكلٍ مختلف. راجِع قسم قراءة مكمِّلة لمزيد من المعلومات.

مصطلحات IndexedDB

قاعدة البيانات
أعلى مستوى في IndexedDB. إنه يحتوي على مخازن الكائنات، والتي تحتوي بدورها على البيانات التي تريد الاستمرار فيها. يمكنك إنشاء قواعد بيانات متعددة بأي أسماء تختارها.
متجر العناصر
حزمة فردية لتخزين البيانات على غرار الجداول في قواعد البيانات الارتباطية. عادةً ما يكون هناك مخزن كائنات واحد لكل نوع (وليس نوع بيانات JavaScript) من البيانات التي تخزِّنها. وعلى عكس جداول قاعدة البيانات، لا يلزم أن تكون أنواع بيانات JavaScript من البيانات في المتجر متسقة. على سبيل المثال، إذا كان أحد التطبيقات يحتوي على متجر عناصر people يحتوي على معلومات عن ثلاثة أشخاص، قد تكون خصائص الفئة العمرية لهؤلاء المستخدمين 53 و'twenty-five' وunknown.
الفهرس
نوع من مخزن العناصر لتنظيم البيانات في مخزن عناصر آخر (يُسمى مخزن العناصر المرجعية) حسب خاصية فردية للبيانات. تستخدم هذه السمة الفهرس لاسترداد السجلات في مخزن الكائنات. على سبيل المثال، إذا كنت تخزن أشخاصًا، ننصحك بجلبهم لاحقًا حسب أسمائهم أو عمرهم أو حيوانهم المفضل.
العملية
تفاعل مع قاعدة البيانات
المعاملة
برنامج تضمين حول عملية أو مجموعة من العمليات لضمان سلامة قاعدة البيانات. إذا فشل أحد الإجراءات في المعاملة، فلن يتم تطبيق أي منها وتعود قاعدة البيانات إلى الحالة التي كانت عليها قبل بدء المعاملة. يجب أن تكون جميع عمليات القراءة أو الكتابة في 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. إذا كان بإمكان الدالة المواصلة، ستستدعي الطريقة openDB() لفتح قاعدة بيانات باسم 'test-db1'. في هذا المثال، تم استبعاد كائن الأحداث الاختياري للحفاظ على تبسيط الأمور، ولكنك تحتاج إلى تحديده للقيام بأي عمل ذي معنى باستخدام قاعدة البيانات المفهرسة.

كيفية التعامل مع متاجر السلع

تحتوي قاعدة بيانات IndexedDB على مخازن عناصر واحدة أو أكثر، يحتوي كل منها على عمود لمفتاح وعمود آخر للبيانات المرتبطة بهذا المفتاح.

إنشاء متاجر العناصر

يجب أن تحتوي قاعدة بيانات IndexedDB ذات البنية الجيدة على مخزن عناصر واحد لكل نوع من البيانات التي تحتاج إلى الاحتفاظ بها. على سبيل المثال، قد يشتمل الموقع الإلكتروني الذي يحتفظ بملفات المستخدمين الشخصية وملاحظاتهم على مخزن عناصر people يحتوي على عناصر person، ومخزن عناصر notes يحتوي على note عنصر.

لضمان تكامل قاعدة البيانات، يمكنك فقط إنشاء أو إزالة مخازن عناصر في عنصر الأحداث في استدعاء openDB(). يعرض كائن الأحداث طريقة upgrade() التي تتيح لك إنشاء مخازن عناصر. استدعِ الطريقة 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();

طريقة تحديد الفهارس

الفهارس هي نوع من مخزن العناصر، ويتم استخدامها لاسترداد البيانات من مخزن الكائنات المرجعية بواسطة خاصية محدّدة. يوجد الفهرس داخل مخزن الكائن المرجعي ويحتوي على نفس البيانات، غير أنه يستخدم الخاصية المحددة كمسار رئيسي بدلاً من المفتاح الأساسي للمخزن المرجعي. يجب إجراء الفهارس عند إنشاء مخازن الكائنات، ويمكن استخدامها لتحديد قيد فريد على بياناتك.

لإنشاء فهرس، استدعِ الطريقة 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() عليه.

كيفية التعامل مع البيانات

يصف هذا القسم كيفية إنشاء البيانات وقراءتها وتحديثها وحذفها. جميع هذه العمليات غير متزامنة، حيث يتم استخدام الوعود التي تستخدم فيها IndexedDB API الطلبات. وهذا يؤدي إلى تبسيط واجهة برمجة التطبيقات. بدلاً من الاستماع إلى الأحداث التي بدأها الطلب، يمكنك استدعاء .then() في كائن قاعدة البيانات الذي يتم عرضه من طريقة openDB() لبدء التفاعلات مع قاعدة البيانات، أو await إنشائها.

يتم تنفيذ جميع عمليات البيانات في IndexedDB داخل المعاملة. ولكل عملية الشكل التالي:

  1. احصل على كائن قاعدة البيانات.
  2. فتح المعاملة في قاعدة البيانات.
  3. فتح متجر العناصر في المعاملة
  4. تنفيذ العملية على مخزن العناصر.

يمكن اعتبار المعاملة على أنها برنامج تضمين آمن حول عملية أو مجموعة من العمليات. وإذا تعذّر إجراء أحد الإجراءات ضمن معاملة معيّنة، يتم التراجع عن كل الإجراءات. تقتصر المعاملات على متجر واحد أو أكثر من متاجر العناصر، والتي تحدِّدها عند فتح المعاملة. يمكن أن تكون للقراءة فقط أو القراءة والكتابة. يشير هذا إلى ما إذا كانت العمليات داخل المعاملة تقرأ البيانات أو تُجري تغييرًا على قاعدة البيانات.

إنشاء البيانات

لإنشاء بيانات، يمكنك استدعاء الإجراء 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();

بعد فتح قاعدة البيانات (وإنشاء مخزن كائنات إذا لزم الأمر)، ستحتاج إلى فتح معاملة من خلال استدعاء طريقة 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 إذا كنت تفضّل ذلك، أو استخدام معاودة الاتصال بالرمز .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();

يعد استرداد صف واحد من قاعدة البيانات أمرًا بسيطًا إلى حد ما: افتح قاعدة البيانات وحدد مخزن الكائنات والقيمة الرئيسية الأساسية للصف الذي تريد الحصول على البيانات منه. بما أنّ طريقة 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() في مخزن العناصر:

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'، ويتم طلب طريقة 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() مؤشرًا يمثل الكائن التالي، وتستمر في التكرار الحلقي حتى تصل إلى نهاية النطاق.

تعيين إصدارات قاعدة البيانات

عند استدعاء الإجراء openDB()، يمكنك تحديد رقم إصدار قاعدة البيانات في المعلمة الثانية. في جميع الأمثلة الواردة في هذا الدليل، تم ضبط الإصدار على 1، ولكن يمكن ترقية قاعدة البيانات إلى إصدار جديد إذا كنت بحاجة إلى تعديلها بطريقة ما. وإذا كان الإصدار المحدّد أكبر من إصدار قاعدة البيانات الحالية، يتم تنفيذ استدعاء upgrade في كائن الحدث، ما يسمح لك بإضافة مخازن عناصر جديدة وفهرستها إلى قاعدة البيانات.

يحتوي الكائن db في استدعاء upgrade على سمة oldVersion خاصة، مشيرًا إلى رقم إصدار قاعدة البيانات التي يمكن للمتصفِّح الوصول إليها. يمكنك تمرير رقم الإصدار هذا في عبارة switch لتنفيذ كتل من التعليمات البرمجية داخل معاودة الاتصال 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

حدود تخزين البيانات