IndexedDB: Una guía para desarrolladores web frontend
Tags: javascript, indexeddb, webdev, frontend
Published: 2024-02-21 16:21:55

Antes de sumergirnos en operaciones con IndexedDB, es crucial entender la importancia de estas funcionalidades en el desarrollo web frontend. IndexedDB no solo ofrece un método eficiente para almacenar y recuperar datos en el navegador, sino que también proporciona características avanzadas que permiten a los desarrolladores construir aplicaciones web más potentes y versátiles.

## ¿Qué es IndexedDB?

IndexedDB es una API de JavaScript que proporciona una forma de almacenar grandes cantidades de datos de manera estructurada en el navegador del usuario. A diferencia de las cookies y el almacenamiento local, que tienen limitaciones significativas en términos de capacidad y estructura, IndexedDB está diseñado para manejar **conjuntos de datos más grandes y complejos**.

## Compatibilidad del Navegador:

IndexedDB ha experimentado un crecimiento significativo en términos de compatibilidad con los navegadores modernos. A medida que la tecnología web ha evolucionado, la mayoría de los navegadores principales ahora admiten IndexedDB, brindando a los desarrolladores una plataforma consistente para almacenar datos en el lado del cliente. Aquí hay una visión general de la compatibilidad de IndexedDB con algunos de los navegadores más utilizados:

![Compatibilidad del Navegador de IndexedDB](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gmlyq7t6maf0iobzcipm.png)

IndexedDB es casi compatible con todos los navegadores modernos. Sin embargo, si estás trabajando con navegadores más antiguos tienes esta forma sencilla de verificarlo en el objeto window:

```javascript
  if (!('indexedDB' in window)) {
    // No puede usar IndexedDB
    console.log("Este navegador no soporta IndexedDB");
    return;
  } else {
    // Puede usar IndexedDB:
    // ...
  }
```

## Ventajas de usar IndexedDB

IndexedDB ofrece varias ventajas significativas para los desarrolladores web que necesitan almacenar datos en el navegador del usuario. Estas ventajas contribuyen a una experiencia de usuario más eficiente y robusta. A continuación, se detallan algunas de las principales ventajas:

**1. Capacidad para grandes cantidades de datos:**

IndexedDB es capaz de manejar conjuntos de datos significativamente grandes en comparación con otras opciones de almacenamiento del lado del cliente, lo que lo convierte en una elección ideal para aplicaciones que manejan datos extensos.

**2. Soporte para transacciones:**

IndexedDB tiene la capacidad para trabajar con transacciones, permitiendo operaciones atómicas y garantizando la coherencia de los datos. Esto es esencial en situaciones donde las operaciones deben ser ejecutadas en conjunto o ninguna de ellas.

**3. Trabajo eficiente fuera de línea:**

IndexedDB facilita el trabajo sin conexión, permitiendo que las aplicaciones accedan y manipulen datos incluso cuando el usuario no está conectado a Internet. Esto es especialmente útil para aplicaciones que requieren funcionalidad offline, como lectores de noticias o aplicaciones de gestión de tareas.

**4. Estructura de datos flexible:**

La flexibilidad de la estructura de datos en IndexedDB, donde puedes almacenar objetos complejos y anidados. Esto facilita la representación de datos complejos y su manipulación en el lado del cliente.

**5. Seguridad y privacidad:**

IndexedDB, al ser parte del navegador, ofrece un nivel de seguridad y privacidad al evitar que otros dominios accedan directamente a los datos almacenados. Además, los datos almacenados no son enviados al servidor a menos que sea necesario.

## Uso básico de IndexedDB:

Exploremos los conceptos fundamentales de cómo trabajar con IndexedDB, desde la creación de una base de datos hasta la manipulación de datos dentro de ella.

Antes de actualizar datos en IndexedDB, es esencial comprender cómo abrir una conexión a la base de datos, iniciar una transacción y trabajar con objetos de almacenamiento. Aquí hay una breve descripción:

**1. Apertura y creación de una base de datos:**

Puedes crear varias bases de datos. Si la base de datos no existe, se creará automáticamente:

```javascript
// Abriendo o creando una base de datos llamada "MiBaseDeDatos" con la versión 1.
const request = indexedDB.open("MiBaseDeDatos", 1);
```

![Apertura y creación de una base de datos](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ow0tvqh4c7iynv3erqc4.png)

**2. Manejo de eventos y devoluciones de llamada (callbacks):**

IndexedDB utiliza eventos para notificar sobre cambios en la base de datos, como aperturas exitosas, actualizaciones de datos, errores, etc. Además, se utilizan devoluciones de llamada para manejar estas situaciones y ejecutar código específico en respuesta a eventos.

En cuanto a los eventos **onsuccess** y **onerror**, se utilizan para manejar el resultado de la apertura de la conexión a la base de datos. Aquí tienes un ejemplo que incluye el manejo de estos eventos:

```javascript
let db;

// Abrir una conexión a la base de datos
const request = indexedDB.open('miBaseDeDatos', 1);

// Manejar éxito al abrir la conexión
request.onsuccess = function (event) {
  db = event.target.result;
  console.log('Conexión exitosa a la base de datos.');

  // Lógica después de abrir la conexión con éxito

  // Por ejemplo, realizar operaciones con la base de datos
};

// Manejar errores al abrir la conexión
request.onerror = function (event) {
  console.error('Error al abrir la conexión:', event.target.error);
};

// Manejar actualización de la versión de la base de datos
request.onupgradeneeded = function (event) {
  const db = event.target.result;

  // Crear o modificar almacenes de objetos en función de la versión
};

// Manejar cambio de versión por otra instancia
db.onversionchange = function (event) {
  db.close();
  console.log('Se ha producido un cambio en la versión de la base de datos. Por favor, recargue la página.');
};

```

El evento **updateneeded** se dispara cuando se abre una conexión a la base de datos y se detecta que la versión proporcionada es mayor que la versión actual de la base de datos o si la base de datos no existe. Es el lugar adecuado para realizar tareas de **actualización** de la estructura de la base de datos, como **crear** o **modificar** objetos de almacenamiento.

El evento **onversionchange** se dispara cuando otra instancia del mismo navegador cambia la versión de la base de datos. Es el lugar adecuado para **cerrar la conexión actual**, ya que la base de datos ha cambiado y se requiere una nueva conexión para reflejar esos cambios.


**3. Creación y Eliminación de Almacenes de Objetos:**

Una base de datos IndexedDB puede contener uno o más almacenes de objetos. Cada almacén de objetos puede contener múltiples registros, y cada registro es un objeto que puede tener propiedades y valores asociados.

```javascript
// Manejar actualización de la versión de la base de datos
request.onupgradeneeded = function (event) {
  const db = event.target.result;

  // Obtener la transacción y crear o modificar almacenes de objetos
  const transaction = event.target.transaction;

  // Crear un nuevo almacén de objetos
  if (!db.objectStoreNames.contains('productos')) {
    const newStore = db.createObjectStore('productos', { keyPath: 'id' });
    newStore.createIndex('porNombre', 'nombre', { unique: false });
  }

  // Eliminar un almacén de objetos existente
  if (db.objectStoreNames.contains('productosAntiguo')) {
    db.deleteObjectStore('productosAntiguo');
  }
};
```

![Creación de Almacenes de Objetos](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/stvqq719knt7ieryv9np.png)

Dentro de **onupgradeneeded**, se obtiene la transacción (event.target.transaction) y se utiliza para crear (**createObjectStore**) o eliminar (**deleteObjectStore**) almacenes de objetos.

Para crear un nuevo almacén de objetos (productos), se utiliza **createObjectStore **y también se crea un índice llamado porNombre basado en la propiedad nombre.

Para eliminar un almacén de objetos existente (productosAntiguo), se utiliza **deleteObjectStore**.

**4. Agregar y Recuperar Datos:**

La capacidad de agregar datos a un almacén de objetos es esencial para cualquier aplicación web. Al hacerlo, podemos almacenar información relevante, como configuraciones de usuario, preferencias o cualquier otro dato necesario para el funcionamiento de la aplicación. La clave para lograr esto reside en el uso de transacciones y solicitudes de almacenamiento en indexedDB. Al abrir una transacción y realizar una solicitud de almacenamiento, podemos insertar nuevos registros o actualizar los existentes en el almacén de objetos.

```javascript
let db;

// Abrir una conexión a la base de datos
const request = indexedDB.open('miBaseDeDatos', 3);

// Manejar éxito al abrir la conexión
request.onsuccess = function (event) {
  db = event.target.result;
  console.log('Conexión exitosa a la base de datos.');

  // Añadir datos al almacén de objetos
  agregarDatos();

  // Recuperar datos del almacén de objetos
  recuperarDatos();
};

// Manejar errores al abrir la conexión
request.onerror = function (event) {
  console.error('Error al abrir la conexión:', event.target.error);
};

// Manejar actualización de la versión de la base de datos
request.onupgradeneeded = function (event) {
  const db = event.target.result;

  // Obtener la transacción y crear o modificar almacenes de objetos
  const transaction = event.target.transaction;

  // Crear un nuevo almacén de objetos
  if (!db.objectStoreNames.contains('productos')) {
    const newStore = db.createObjectStore('productos', { keyPath: 'id' });
    newStore.createIndex('porNombre', 'nombre', { unique: false });
  }
};

// Manejar cambio de versión por otra instancia
db.onversionchange = function (event) {
  db.close();
  console.log('Se ha producido un cambio en la versión de la base de datos. Por favor, recargue la página.');
};

// Función para añadir datos al almacén de objetos
function agregarDatos() {
  const transaction = db.transaction(['productos'], 'readwrite');
  const store = transaction.objectStore('productos');

  // Añadir un nuevo objeto
  const requestAdd = store.add({ id: 1, nombre: 'Ejemplo 1', cantidad: 30 });

  requestAdd.onsuccess = function (event) {
    console.log('Datos añadidos con éxito.');
  };

  requestAdd.onerror = function (event) {
    console.error('Error al añadir datos:', event.target.error);
  };
}

// Función para recuperar datos del almacén de objetos
function recuperarDatos() {
  const transaction = db.transaction(['productos'], 'readonly');
  const store = transaction.objectStore('productos');

  // Recuperar objeto por clave primaria (id)
  const requestGet = store.get(1);

  requestGet.onsuccess = function (event) {
    const result = event.target.result;
    if (result) {
      console.log('Datos recuperados:', result);
    } else {
      console.log('No se encontraron datos con la clave primaria proporcionada.');
    }
  };

  requestGet.onerror = function (event) {
    console.error('Error al recuperar datos:', event.target.error);
  };
}
```

La recuperación de datos es igualmente crucial, ya que permite a la aplicación web acceder y mostrar la información almacenada de manera eficiente. En indexedDB, esto se logra mediante la apertura de una transacción de solo lectura y la realización de solicitudes de recuperación de datos. Podemos realizar búsquedas específicas utilizando índices o simplemente recuperar todos los registros presentes en el almacén de objetos.

La función _agregarDatos_ realiza una transacción de escritura ('readwrite') para añadir un nuevo objeto al almacén de objetos productos usando el método **add**.

La función _recuperarDatos_ realiza una transacción de solo lectura ('readonly') para recuperar un objeto del almacén de objetos productos usando el método **get**.

Adapta el código según tus necesidades específicas y la estructura de tu base de datos. Ten en cuenta que estas operaciones deben realizarse dentro de transacciones y se manejan mediante eventos **onsuccess** y **onerror**.

**5. Actualizar y Eliminar datos:**

La actualización y eliminación de datos en IndexedDB implica modificar registros existentes en la base de datos. Este proceso generalmente se realiza mediante transacciones.

```javascript
let db;

// Abrir una conexión a la base de datos
const request = indexedDB.open('miBaseDeDatos', 4);

// Manejar éxito al abrir la conexión
request.onsuccess = function (event) {
  db = event.target.result;
  console.log('Conexión exitosa a la base de datos.');

  // Actualizar datos en el almacén de objetos
  actualizarDatos();

  // Eliminar datos en el almacén de objetos
  eliminarDatos();
};

// Manejar errores al abrir la conexión
request.onerror = function (event) {
  console.error('Error al abrir la conexión:', event.target.error);
};

// Manejar actualización de la versión de la base de datos
request.onupgradeneeded = function (event) {
  const db = event.target.result;

  // Obtener la transacción y crear o modificar almacenes de objetos
  const transaction = event.target.transaction;

  // Crear un nuevo almacén de objetos
  if (!db.objectStoreNames.contains('productos')) {
    const newStore = db.createObjectStore('productos', { keyPath: 'id' });
    newStore.createIndex('porNombre', 'nombre', { unique: false });
  }
};

// Manejar cambio de versión por otra instancia
db.onversionchange = function (event) {
  db.close();
  console.log('Se ha producido un cambio en la versión de la base de datos. Por favor, recargue la página.');
};

// Función para actualizar datos en el almacén de objetos
function actualizarDatos() {
  const transaction = db.transaction(['productos'], 'readwrite');
  const store = transaction.objectStore('productos');

  // Actualizar un objeto existente
  const requestUpdate = store.put({ id: 1, nombre: 'Ejemplo 1 (actualizado)', cantidad: 35 });

  requestUpdate.onsuccess = function (event) {
    console.log('Datos actualizados con éxito.');
  };

  requestUpdate.onerror = function (event) {
    console.error('Error al actualizar datos:', event.target.error);
  };
}

// Función para eliminar datos en el almacén de objetos
function eliminarDatos() {
  const transaction = db.transaction(['productos'], 'readwrite');
  const store = transaction.objectStore('productos');

  // Eliminar un objeto por clave primaria (id)
  const requestDelete = store.delete(1);

  requestDelete.onsuccess = function (event) {
    console.log('Datos eliminados con éxito.');
  };

  requestDelete.onerror = function (event) {
    console.error('Error al eliminar datos:', event.target.error);
  };
}
```

La función _actualizarDatos_ realiza una transacción de escritura ('readwrite') para actualizar un objeto existente en el almacén de objetos _productos_ utilizando el método **put**.

La función _eliminarDatos_ realiza una transacción de escritura ('readwrite') para eliminar un objeto del almacén de objetos _productos_ utilizando el método **delete**.

Ambas operaciones se manejan mediante eventos **onsuccess ** y **onerror**. Ajusta el código según tus necesidades y la estructura de tu base de datos.

En conjunto, la capacidad de añadir, actualizar, recuperar y eliminar datos en indexedDB proporciona una base sólida para el desarrollo de aplicaciones web interactivas y eficientes, asegurándonos de aprovechar al máximo las funcionalidades que indexedDB ofrece para el manejo avanzado de datos en el navegador.

Estos son ejemplos básicos y pueden variar según los requisitos específicos de tu aplicación. Recuerda manejar adecuadamente las transacciones, cerrar conexiones cuando no se necesiten y gestionar errores para un código robusto y confiable.

**6. Indices y Punteros (Cursores):**

Los **índices** en IndexedDB son estructuras adicionales que permiten la búsqueda eficiente de datos en campos específicos de los objetos almacenados en un almacén de objetos. Estos índices mejoran el rendimiento de las consultas que se basan en criterios de búsqueda, ya que permiten acceder a los datos de manera más rápida.

Al crear un índice, puedes especificar qué propiedad o campo del objeto almacenado debe indexarse. Esto se realiza generalmente dentro del evento _onupgradeneeded_ al abrir la conexión a la base de datos.

```javascript
const request = indexedDB.open('miBaseDeDatos', 5);

request.onupgradeneeded = function (event) {
  const db = event.target.result;

  if (!db.objectStoreNames.contains('productos')) {
    const objectStore = db.createObjectStore('productos', { keyPath: 'id' });

    // Crear un índice por el campo 'nombre'
    objectStore.createIndex('porNombre', 'nombre', { unique: false });
  }
};
```
Se crea un índice llamado '**porNombre**' en el almacén de objetos _productos_ basado en el campo nombre.

En el contexto de IndexedDB, no hay un concepto directo llamado "**punteros**". Sin embargo, el API de IndexedDB utiliza referencias de **cursor** para iterar sobre conjuntos de datos. Los **cursores** actúan como "punteros" que apuntan a ubicaciones específicas en un almacén de objetos o índice.

```javascript
const transaction = db.transaction(['productos'], 'readonly');
const objectStore = transaction.objectStore('productos');
const index = objectStore.index('porNombre');

const cursorRequest = index.openCursor();

cursorRequest.onsuccess = function (event) {
  const cursor = event.target.result;

  if (cursor) {
    // Acceder a los datos utilizando el cursor
    console.log('ID:', cursor.primaryKey, 'Nombre:', cursor.value.nombre);

    // Mover al siguiente registro
    cursor.continue();
  } else {
    console.log('Fin del cursor.');
  }
};

cursorRequest.onerror = function (event) {
  console.error('Error al abrir el cursor:', event.target.error);
};
```

Se utiliza un cursor para recorrer los datos del índice 'porNombre' del almacén de objetos _productos_. Cada llamada a **cursor.continue()** mueve el cursor al siguiente registro.

Recuerda que el uso de índices y cursores depende de los requisitos específicos de tu aplicación y de cómo desees acceder y manipular los datos almacenados en IndexedDB.

**6. Ejemplo práctico de Indices y Cursores:**

Vamos a ver un ejemplo práctico de cómo usar índices en IndexedDB. Supongamos que tienes una base de datos que almacena información sobre libros, y quieres buscar libros por su autor. Vamos a crear un índice para el campo del autor y realizar búsquedas eficientes.

```javascript
const request = indexedDB.open('biblioteca', 1);

request.onupgradeneeded = function (event) {
  const db = event.target.result;

  // Crear almacén de objetos
  const bookStore = db.createObjectStore('libros', { keyPath: 'id' });

  // Crear un índice por el campo 'autor'
  bookStore.createIndex('porAutor', 'autor', { unique: false });

  // Agregar algunos libros de ejemplo
  bookStore.add({ id: 1, titulo: 'Don Quijote', autor: 'Miguel de Cervantes' });
  bookStore.add({ id: 2, titulo: 'Cien años de soledad', autor: 'Gabriel García Márquez' });
  bookStore.add({ id: 3, titulo: '1984', autor: 'George Orwell' });
};

request.onsuccess = function (event) {
  const db = event.target.result;

  // Realizar una búsqueda utilizando el índice
  const transaction = db.transaction(['libros'], 'readonly');
  const bookStore = transaction.objectStore('libros');
  const index = bookStore.index('porAutor');

  // Buscar libros por autor
  const query = index.openCursor(IDBKeyRange.only('Miguel de Cervantes'));

  query.onsuccess = function (event) {
    const cursor = event.target.result;

    if (cursor) {
      // Acceder a los datos utilizando el cursor
      console.log('ID:', cursor.primaryKey, 'Título:', cursor.value.titulo, 'Autor:', cursor.value.autor);

      // Mover al siguiente registro
      cursor.continue();
    } else {
      console.log('Fin de la búsqueda.');
    }
  };

  query.onerror = function (event) {
    console.error('Error al realizar la búsqueda:', event.target.error);
  };
};
```

- Se crea un almacén de objetos llamado 'libros'.
- Se crea un índice llamado 'porAutor' basado en el campo 'autor'.
- Se agregan algunos libros de ejemplo al almacén de objetos.
- Se realiza una búsqueda utilizando el índice 'porAutor' para encontrar todos los libros escritos por 'Miguel de Cervantes'.
- El índice 'porAutor' permite realizar búsquedas eficientes basadas en el campo 'autor' sin tener que recorrer todos los registros del almacén de objetos. 

Este es un ejemplo simple, pero en aplicaciones más complejas, los índices son esenciales para optimizar la recuperación de datos.

## En conclusión

IndexedDB es una API poderosa en el lado del cliente para el almacenamiento de datos en navegadores web. Aquí hay algunas conclusiones clave:

**Almacenamiento de Datos:**

IndexedDB proporciona una forma robusta de almacenar datos estructurados en el navegador, lo que permite a las aplicaciones web trabajar con grandes conjuntos de datos de manera eficiente.

**Manejo de Versiones:**

La gestión de versiones en IndexedDB se realiza a través de eventos como _onupgradeneeded_. Esto permite realizar cambios en la estructura de la base de datos de manera controlada y garantiza la compatibilidad hacia adelante.

**Uso de Índices:**

La creación de índices en campos específicos facilita la búsqueda y recuperación eficiente de datos. Esto es crucial para optimizar consultas y mejorar el rendimiento en aplicaciones que manejan grandes cantidades de información.

**Transacciones:**

El trabajo con transacciones en IndexedDB es fundamental para garantizar la coherencia y consistencia de los datos. Las transacciones pueden ser de solo lectura (_readonly_) o de escritura (_readwrite_), dependiendo de la operación que estés realizando.

**Manejo de Eventos y Devoluciones de Llamada:**

El manejo de eventos y devoluciones de llamada, como _onsuccess_ y _onerror_, es esencial para manejar situaciones como aperturas exitosas, errores y cambios en la versión de la base de datos.

**Operaciones Básicas:**

IndexedDB permite realizar operaciones básicas como añadir, recuperar, actualizar y eliminar datos en el almacén de objetos. Estas operaciones se realizan dentro de transacciones.

**Punteros y Cursores:**

Los cursores actúan como "punteros" para recorrer conjuntos de datos en almacenes de objetos e índices.

**Cierre y Bloqueo:**

El evento _onversionchange_ se utiliza para manejar situaciones en las que otra instancia de la base de datos ha cambiado la versión, y se cierra la conexión actual para permitir la actualización.

En general, IndexedDB es una herramienta valiosa para desarrolladores web que necesitan almacenar y gestionar datos en el lado del cliente de manera eficiente y escalable. Su capacidad para trabajar con grandes conjuntos de datos y proporcionar una interfaz versátil para la manipulación de datos hace que sea una opción sólida para aplicaciones web modernas.