Trabaja con IndexedDB

Una guía sobre los conceptos básicos de IndexedDB.

En esta guía, se abordan los conceptos básicos de la API de IndexedDB. Estamos usando la biblioteca IndexedDB Promise de Jake Archibald, que es muy similar a la API de IndexedDB, pero utiliza promesas (que puedes await para obtener una sintaxis más breve). Esto simplifica la API y, al mismo tiempo, mantiene su estructura.

¿Qué es IndexedDB?

IndexedDB es un sistema de almacenamiento NoSQL a gran escala que permite almacenar casi cualquier elemento en el navegador del usuario. Además de las acciones habituales de search, get y put, IndexedDB también admite transacciones. Esta es la definición de IndexedDB en MDN:

IndexedDB es una API de bajo nivel para el almacenamiento del cliente de cantidades significativas de datos estructurados, incluidos archivos o BLOB. Esta API usa índices para habilitar búsquedas de alto rendimiento de estos datos. Si bien el almacenamiento del DOM es útil para almacenar pequeñas cantidades de datos, es menos útil para almacenar grandes cantidades de datos estructurados. IndexedDB brinda una solución.

Cada base de datos de IndexedDB es única en un origen (por lo general, es el dominio o subdominio del sitio), lo que significa que ningún otro origen puede acceder a ella. Los límites de almacenamiento de datos suelen ser bastante amplios, si existen, pero los distintos navegadores manejan los límites y la expulsión de datos de manera diferente. Consulta la sección Lecturas adicionales para obtener más información.

Términos de IndexedDB

Base de datos
Es el nivel más alto de IndexedDB. Contiene los almacenes de objetos que, a su vez, contienen los datos que deseas conservar. Puedes crear varias bases de datos con los nombres que elijas.
Almacén de objetos
Un bucket individual para almacenar datos. Los almacenes de objetos son similares a las tablas de las bases de datos relacionales tradicionales. Por lo general, hay un almacén de objetos para cada tipo (no el tipo de datos de JavaScript) que almacenas. Por ejemplo, en el caso de una app que conserva entradas de blog y perfiles de usuario, puedes imaginar dos almacenes de objetos. A diferencia de las tablas de las bases de datos tradicionales, no es necesario que los tipos de datos reales de JavaScript en el almacén sean coherentes (por ejemplo, si hay tres personas en el almacén de objetos people, sus propiedades de edad podrían ser 53, 'twenty-five' y unknown).
Índice
Es un tipo de almacén de objetos para organizar datos en otro almacén de objetos (llamado almacén de objetos de referencia) según una propiedad individual de los datos. El índice se usa para recuperar registros en el depósito de objetos de esta propiedad. Por ejemplo, si almacenas personas, es posible que quieras buscarlas más tarde por su nombre, edad o animal favorito.
Operación
Es una interacción con la base de datos.
Transacción
Es un wrapper alrededor de una operación o grupo de operaciones que garantiza la integridad de la base de datos. Si falla una de las acciones dentro de una transacción, no se aplica ninguna de ellas y la base de datos vuelve al estado en el que estaba antes de que comenzara la transacción. Todas las operaciones de lectura o escritura en IndexedDB deben ser parte de una transacción. Esto permite realizar operaciones atómicas de lectura, modificación y escritura sin tener que preocuparse de que otros subprocesos actúen en la base de datos al mismo tiempo.
Cursor
Es un mecanismo para iterar en varios registros de una base de datos.

Cómo comprobar la compatibilidad con IndexedDB

IndexedDB es casi compatible universalmente. Sin embargo, si estás trabajando con navegadores más antiguos, es una buena idea detectar la compatibilidad con funciones por si acaso. La forma más sencilla es verificar el objeto 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();

Cómo abrir una base de datos

Con IndexedDB, puedes crear varias bases de datos con el nombre que elijas. Si la base de datos no existe cuando intentas abrirla, se creará automáticamente. Para abrir una base de datos, puedes usar el método openDB() con la biblioteca 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();

Este método muestra una promesa que se resuelve en un objeto de base de datos. Cuando usas el openDB(), proporciona un nombre, un número de versión y un objeto de eventos para configurar la base de datos.

Este es un ejemplo del método openDB() en contexto:

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

Debes colocar la verificación de compatibilidad con IndexedDB en la parte superior de la función anónima. Si el navegador no admite IndexedDB, se cerrará la función. Luego, llamarás al método openDB() para abrir una base de datos llamada 'test-db1'. En este ejemplo, el objeto de eventos opcional se omitió para simplificar el proceso, pero, con el tiempo, deberás especificarlo para realizar cualquier trabajo importante con IndexedDB.

Cómo trabajar con almacenes de objetos

Una base de datos IndexedDB contiene uno o más almacenes de objetos. El concepto de un almacén de objetos es similar al de una tabla en una base de datos SQL. Al igual que las tablas de SQL, un almacén de objetos contiene filas y columnas, pero en IndexedDB, hay menos flexibilidad en la cantidad de columnas que un almacén de objetos IndexedDB contiene una columna para una clave y otra columna para los datos asociados con esa clave.

Crea almacenes de objetos

Tomemos, por ejemplo, un sitio que conserva perfiles de usuario y notas. Imagina un almacén de objetos people que contiene objetos person y un almacén de objetos notes. Una base de datos IndexedDB bien estructurada debe tener un almacén de objetos para cada tipo de datos que se deben conservar.

Para garantizar la integridad de la base de datos, los almacenes de objetos solo se pueden crear y quitar en el objeto de eventos en una llamada a openDB(). El objeto de eventos expone un método upgrade() que proporciona una manera de crear almacenes de objetos. Llama al método createObjectStore() en el método upgrade() para crear el almacén de objetos:

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

Este método toma el nombre del almacén de objetos, así como un objeto de configuración opcional que te permite definir varias propiedades para el almacén de objetos.

El siguiente es un ejemplo de cómo se usa el método 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();

En este ejemplo, se pasa un objeto de eventos al método openDB() para crear el almacén de objetos y, al igual que antes, el trabajo de crear el almacén de objetos se realiza en el método upgrade() del objeto de evento. Sin embargo, el navegador arrojará un error si intentas crear un almacén de objetos que ya existe, por lo que une el método createObjectStore() en una sentencia if que verifique si el almacén de objetos existe. Dentro del bloque if, llama a createObjectStore() para crear un almacén de objetos llamado 'firstOS'.

Cómo definir claves primarias

Cuando defines el almacenamiento de objetos, puedes definir cómo los datos se identifican de manera única en el almacén usando una clave primaria. Para definir una clave primaria, puedes definir una ruta de acceso de la clave o usar un generador de claves.

Una ruta de acceso de la clave es una propiedad que siempre existe y contiene un valor único. Por ejemplo, en el caso de un almacén de objetos people, puedes elegir la dirección de correo electrónico como la ruta de acceso de la clave:

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

En este ejemplo, se crea un almacén de objetos llamado 'people' y se asigna la propiedad email como clave primaria en la opción keyPath.

También puedes usar un generador de claves, como autoIncrement. El generador de claves crea un valor único para cada objeto que se agrega al almacén de objetos. De forma predeterminada, si no especificas una clave, IndexedDB crea una clave y la almacena por separado de los datos.

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

En este ejemplo, se crea un almacén de objetos llamado 'notes' y se establece la clave primaria para que se asigne automáticamente como un número de incremento automático.

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

Este ejemplo es similar al anterior, pero esta vez el valor de incremento automático se asigna de forma explícita a una propiedad llamada 'id'.

Elegir qué método usar para definir la clave depende de tus datos. Si tus datos tienen una propiedad que siempre es única, puedes establecerla como keyPath para aplicarla. De lo contrario, usar un valor de incremento automático tiene sentido.

El siguiente código crea tres almacenes de objetos que demuestran las distintas maneras de definir claves primarias en los almacenes de objetos:

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

Cómo definir índices

Los índices son un tipo de almacén de objetos que se usa para recuperar datos del almacén de objetos de referencia mediante una propiedad especificada. Un índice se encuentra dentro del almacén de objetos de referencia y contiene los mismos datos, pero usa la propiedad especificada como su ruta de acceso de la clave en lugar de la clave primaria del almacén de referencia. Los índices se deben generar cuando creas tus almacenes de objetos y también se pueden usar para definir una restricción única en tus datos.

Para crear un índice, llama al método createIndex() en una instancia del almacén de objetos:

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

Este método crea y muestra un objeto de índice. El método createIndex() de la instancia del almacén de objetos toma el nombre del índice nuevo como primer argumento, y el segundo argumento a la propiedad de los datos que quieres indexar. El argumento final te permite definir dos opciones que determinan cómo funciona el índice: unique y multiEntry. Si unique se configura como true, el índice no permite valores duplicados para una sola clave. A continuación, multiEntry determina cómo se comporta createIndex() cuando la propiedad indexada es un array. Si se establece en true, createIndex() agrega una entrada en el índice para cada elemento del array. De lo contrario, se agrega una sola entrada que contiene el array.

Por ejemplo:

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

En este ejemplo, los almacenes de objetos 'people' y 'notes' tienen índices. Para crear los índices, primero asigna el resultado de createObjectStore() (que es un objeto de almacenamiento de objetos) a una variable para poder llamar a createIndex() en ella.

Cómo trabajar con datos

En esta sección, se describe cómo crear, leer, actualizar y borrar datos. Todas estas operaciones son asíncronas, con promesas en las que la API de IndexedDB usa solicitudes. Esto simplifica la API. En lugar de detectar eventos activados por la solicitud, puedes llamar a .then() en el objeto de la base de datos que muestra el método openDB() para iniciar interacciones con la base de datos o a await su creación.

Todas las operaciones de datos de IndexedDB se llevan a cabo dentro de una transacción. Cada operación tiene el siguiente formato:

  1. Obtiene el objeto de la base de datos.
  2. Abrir transacción en la base de datos.
  3. Abrir almacén de objetos en la transacción
  4. Realiza operaciones en el almacén de objetos.

Una transacción puede considerarse un wrapper seguro alrededor de una operación o un grupo de operaciones. Si falla una de las acciones de una transacción, se revierten todas las acciones. Las transacciones son específicas de uno o más almacenes de objetos, que defines cuando abres la transacción. Pueden ser de solo lectura o de lectura y escritura. Esto significa si las operaciones dentro de la transacción leen los datos o realizan un cambio en la base de datos.

Crea datos

Para crear datos, llama al método add() en la instancia de base de datos y pasa los datos que deseas agregar. El primer argumento del método add() es el almacén de objetos al que deseas agregar los datos, y el segundo argumento es un objeto que contiene los campos y los datos asociados que deseas agregar. Este es el ejemplo más sencillo, en el que se agrega una sola fila de datos:

import {openDB} from 'idb';

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

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

addItemToStore();

Cada llamada a add() se produce dentro de una transacción, por lo que, incluso si la promesa se resuelve correctamente, no necesariamente significa que la operación funcionó. Recuerda que si falla una de las acciones de la transacción, se revierten todas las operaciones de la transacción.

Para asegurarte de que se haya realizado la operación de agregar, debes verificar si se completó toda la transacción con el método transaction.done(). Esta es una promesa que se resuelve cuando se completa la transacción y se rechaza si la transacción se produce un error. Ten en cuenta que este método no cierra la transacción. La transacción se completa por sí sola. Debes realizar esta verificación para todas las operaciones de “escritura”, ya que es la única forma de saber si los cambios en la base de datos se realizaron realmente.

En el siguiente código, se muestra el uso del método add(), pero esta vez con una transacción:

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

Una vez que abras la base de datos (y crees un almacén de objetos, si es necesario), deberás abrir una transacción llamando al método transaction(). Este método toma un argumento para la tienda con la que quieres realizar la transacción y el modo. En este caso, nos interesa escribir a la tienda, por lo que se especifica 'readwrite' en el ejemplo anterior.

El siguiente paso es comenzar a agregar artículos a la tienda como parte de la transacción. En el ejemplo anterior, se trata de tres operaciones en el almacén 'foods' en las que cada una muestra una promesa:

  1. Agregando el registro de un sabroso sándwich.
  2. Se está agregando un registro para algunos huevos.
  3. Esto indica que se completó la transacción (tx.done).

Debido a que todas estas acciones se basan en promesas, debemos esperar a que todas finalicen. Pasar estas promesas a Promise.all es una forma agradable y ergonómica de hacerlo. Promise.all acepta un array de promesas y finaliza cuando se resuelvan todas las que se le pasaron.

Para los dos registros que se agregan, la interfaz store de la instancia de transacción tiene un método add al que se puede llamar, y los datos se pasan a cada uno. La llamada a Promise.all en sí se puede await y finalizará cuando se complete la transacción.

Cómo leer datos

Para leer datos, llama al método get() en la instancia de base de datos que recuperaste con el método openDB(). get() toma el nombre del almacén, así como el valor de la clave primaria del objeto que deseas recuperar del almacén. A continuación, se muestra un ejemplo básico:

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

Al igual que con add(), el método get() muestra una promesa, por lo que puedes usar await si lo prefieres o usar la devolución de llamada .then() que ofrecen todas las promesas si no deseas hacerlo.

En el siguiente ejemplo, se usa el método get() en el almacén de objetos 'foods' de la base de datos 'test-db4' para obtener una sola fila con la clave primaria '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();

Recuperar una sola fila de la base de datos es bastante sencillo: abres la base de datos y especificas el almacén de objetos y el valor de la clave primaria de la fila de la que quieres obtener datos. Como el método get() muestra una promesa, puedes usar await.

Cómo actualizar datos

Para actualizar datos, llama al método put() en el almacén de objetos. El método put() es similar al método add() y también se puede usar en lugar de add() para crear datos en el almacén de objetos. Este es el ejemplo más simple del uso de put() para actualizar una fila en un almacén de objetos por su valor de clave primaria:

import {openDB} from 'idb';

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

  // Update a value from in an object store with an in-line 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();

Al igual que otros métodos, este método muestra una promesa. También puedes usar put() como parte de una transacción, al igual que lo harías con el método add(). A continuación, se muestra un ejemplo con la tienda 'foods' anterior, excepto que actualizamos el precio del sándwich y los huevos:

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

La forma en que se actualizan los elementos depende de cómo configures una clave. Si configuras una keyPath, cada fila del almacén de objetos se asocia con lo que se conoce como una clave intercalada. En el ejemplo anterior, se actualizan las filas en función de esta clave y, cuando actualices filas en esta situación, deberás especificar esa clave para que se actualice el elemento apropiado en el almacén de objetos. Para crear una clave fuera de línea, se configura una autoIncrement como clave primaria.

Borra datos

Para borrar datos, llama al método delete() en el almacén de objetos:

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

Al igual que add() y put(), también se puede usar como parte de una transacción:

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

La estructura de la interacción de la base de datos es la misma que la de las otras operaciones. Ten en cuenta que, para verificar nuevamente que se haya completado la transacción, incluye el método tx.done en el array que pasas a Promise.all para asegurarte de que se haya llevado a cabo la eliminación.

Obtén todos los datos

Hasta ahora, solo recuperaste objetos de la tienda de a uno a la vez. También puedes recuperar todos los datos (o un subconjunto) de un almacén o índice de objetos con el método getAll() o con cursores.

Usa el método getAll()

La forma más sencilla de recuperar todos los datos de un almacén de objetos es llamar al método getAll() en el almacén o el índice de objetos, de la siguiente manera:

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

Este método muestra todos los objetos en el almacén de objetos, sin restricciones de ningún tipo. Es la forma más directa de obtener todos los valores de un almacén de objetos, pero también la menos flexible.

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

Aquí, se llama a getAll() en el almacén de objetos 'foods'. Esto muestra todos los objetos de 'foods' del almacén ordenados por la clave primaria.

Cómo usar cursores

Otra forma de recuperar todos los datos (que te da la mayor flexibilidad que solo obtener todo a la vez) es usar un cursor. El cursor selecciona cada objeto de un almacén o índice uno por uno, lo que te permite hacer algo con los datos a medida que se seleccionan. Los cursores, al igual que las otras operaciones de la base de datos, trabajan dentro de transacciones.

Para crear el cursor, llama al método openCursor() en el almacén de objetos. Esto se hace como parte de una transacción. Con el almacén 'foods' de los ejemplos anteriores, así es como podrías hacer avanzar un cursor por todas las filas de datos en un almacén de objetos:

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

En este caso, la transacción se abre en modo 'readonly' y se llama al método openCursor. En un bucle while posterior, la fila en la posición actual del cursor puede tener sus propiedades key y value leídas, y puedes operar con esos valores de la manera que tenga más sentido para tu aplicación. Cuando esté todo listo, puedes llamar al método continue() del objeto cursor para ir a la siguiente fila, y el bucle while finalizará una vez que se alcance el final del conjunto de datos.

Cómo usar cursores con índices y rangos

Puedes obtener todos los datos de dos maneras diferentes, pero ¿qué pasa si solo quieres un subconjunto de datos basado en una propiedad en particular? Aquí es donde entran en juego los índices. Los índices te permiten recuperar los datos de un almacén de objetos mediante una propiedad que no sea la clave primaria. Puedes crear un índice en cualquier propiedad (que se convierte en el keyPath del índice), especificar un rango en esa propiedad y obtener los datos dentro del rango con el método getAll() o un cursor.

Define el rango con el objeto IDBKeyRange. Este objeto tiene cinco métodos que se usan para definir los límites del rango:

Como era de esperar, los métodos upperBound() y lowerBound() especifican los límites inferior y superior del rango.

IDBKeyRange.lowerBound(indexKey);

o:

IDBKeyRange.upperBound(indexKey);

Cada una toma un argumento, que es el valor keyPath del índice del elemento que quieres especificar como límite superior o inferior.

El método bound() se usa para especificar un límite inferior y superior, y toma el límite inferior como primer argumento:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

El rango de estas funciones es inclusivo de forma predeterminada, pero se puede especificar como exclusivo si pasas true como el segundo argumento (o el tercero y el cuarto en el caso de bound(), para los límites inferior y superior, respectivamente). Un rango inclusivo incluye los datos en los límites del rango. Un rango exclusivo no.

Veamos un ejemplo. Para esta demostración, creaste un índice en la propiedad 'price' del almacén de objetos 'foods'. También agregaste un formulario pequeño con dos entradas para los límites inferior y superior del rango. Imagina que estás pasando los límites inferior y superior a la función como números de punto flotante que representan los precios:

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

El código primero obtiene los valores de los límites y verifica si existen. El siguiente bloque de código decide qué método usar para limitar el rango en función de los valores. En la interacción de la base de datos, abre el almacén de objetos en la transacción como de costumbre y, luego, abre el índice 'price' en el almacén de objetos. El índice 'price' te permite buscar los elementos por precio.

Luego, abre un cursor en el índice y pasa el rango. El cursor ahora muestra una promesa que representa el primer objeto en el rango o undefined si no hay datos dentro del rango. El método cursor.continue() muestra un cursor que representa el siguiente objeto y así sucesivamente a través del bucle hasta llegar al final del rango.

Usa el control de versiones de la base de datos

Cuando llamas al método openDB(), puedes especificar el número de versión de la base de datos en el segundo parámetro. En todos los ejemplos de esta guía, la versión se estableció como 1, pero una base de datos se puede actualizar a una versión nueva si necesitas modificarla de alguna manera. Si la versión especificada es superior a la de la base de datos existente, se ejecutará la devolución de llamada upgrade en el objeto de evento, lo que te permitirá agregar índices y almacenes de objetos nuevos a la base de datos.

El objeto db de la devolución de llamada upgrade tiene una propiedad especial oldVersion, que indica el número de versión actual de la base de datos existente en el navegador. Puedes pasar este número de versión a una sentencia switch para ejecutar bloques de código dentro de la devolución de llamada upgrade según el número de versión de la base de datos existente. Por ejemplo:

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');
    }
  }
});

En este ejemplo, se establece la versión más reciente de la base de datos en 2. Cuando se ejecuta este código por primera vez, y como la base de datos aún no existe en el navegador, oldVersion tiene el valor 0, y la sentencia switch comienza en case 0. En el ejemplo, se agrega un almacén de objetos 'store' a la base de datos.

Para crear un índice 'description' en el almacén de objetos 'store', actualiza el número de versión y agrega un nuevo bloque case de la siguiente manera:

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');
    }
  }
});

Si suponemos que la base de datos que creaste en el ejemplo anterior aún existe en el navegador, cuando esto ejecuta oldVersion es 2. Se omiten case 0 y case 1, y el navegador ejecuta el código en case 2, lo que crea un índice 'description'. Cuando todo esto haya terminado, el navegador tendrá una base de datos en la versión 3 que contiene un almacén de objetos 'store' con índices 'name' y 'description'.

Lecturas adicionales

Los siguientes recursos pueden proporcionar un poco más de información y contexto cuando se trata del uso de IndexedDB.

Documentación de IndexedDB

Límites de almacenamiento de datos