IndexedDB en JavaScript: guía del almacenamiento en el navegador

IndexedDB es la base de datos clave/valor que llevan todos los navegadores modernos. Esta introducción cubre cómo crear la base, añadir y leer datos con transacciones, migrar de una versión a otra y eliminar almacenes de objetos, con la librería idb de Jake Archibald.

IndexedDB en JavaScript: guía del almacenamiento en el navegador

IndexedDB es una de las soluciones de almacenamiento que los navegadores han ido incorporando con los años.

Es una base de datos clave/valor (una base de datos noSQL) considerada la solución definitiva para guardar datos en el navegador.

Es una API asíncrona, lo que significa que su ejecución no bloquea el resto del programa, con la mejora de experiencia que eso supone para el usuario. Puede guardar una cantidad indefinida de datos, aunque a partir de cierto umbral se pide al usuario que autorice al sitio a almacenar más.

Existen otros métodos, las cookies y el Web Storage ( localStorage y sessionStorage ). El almacenamiento local o de sesión tiene el inconveniente de estar limitado a un tamaño pequeño, ya que los navegadores ofrecen de 2MB a 10MB de espacio por sitio.

Aunque técnicamente puedes crear varias bases de datos por sitio, lo normal es crear una sola y, dentro de ella, varios almacenes de objetos.

Una base de datos es privada de un dominio, de modo que ningún otro sitio puede acceder a una base IndexedDB ajena.

Cada almacén de objetos suele contener un conjunto de cosas, que pueden ser:

  • objects
  • arrays
  • strings
  • numbers
  • dates

Por ejemplo, puedes tener un almacén con los mensajes y otro con los comentarios.

Un almacén contiene una serie de objetos que tienen una clave única, que representa la forma de identificar cada objeto.

Puedes modificar esos objetos mediante transacciones, con operaciones de añadido, modificación, borrado e iteración sobre los elementos que contienen.

Desde la llegada de las Promises en ES6, y el paso posterior de las API a las promesas, la API de IndexedDB parece un poco anticuada.

No hay nada malo en ello, pero en todos los ejemplos que voy a explicar usaré la librería basada en promesas IndexedDB de Jake Archibald, una capa fina sobre la API de IndexedDB que la hace mucho más cómoda de usar.

Esta librería se usa también en todos los ejemplos sobre IndexedDB del sitio Google Developers


Crear una base de datos IndexedDB

La forma más sencilla es usar unpkg, añadiéndolo a la cabecera de la página:

javascript
<script type="module">
import { openDB, deleteDB } from 'https://unpkg.com/idb?module'
</script>

Antes de usar la API de IndexedDB, comprueba siempre que el navegador la soporta: aunque está ampliamente disponible, nunca sabes con qué navegador llega el usuario.

javascript
(async () => {
  'use strict'

if (!('indexedDB' in window)) {
    console.warn('IndexedDB not supported')
    return
  }
})()

Cómo crear una base de datos IndexedDB

Usa openDB():

javascript
(async () => {
  //...

const dbName="mydbname"
  const storeName="store1"
  const version = 1
  const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
    }
  })
})()

Los 2 primeros parámetros son el nombre de la base de datos y la versión. El tercero, opcional, es un objeto que contiene una función que solo se llama si el número de versión es superior al de la base de datos instalada. En el cuerpo de esa función puedes actualizar la estructura (almacenes e índices) de la base de datos.

Añadir datos a un almacén

Añadir datos al crear el almacén: la inicialización

Para añadir datos hay que usar el método put del almacén de objetos, pero antes tenemos que inicializarlo con db.createObjectStore() en el momento de crearlo.

Al usar put, el valor es el primer argumento y la clave el segundo. Esto es así porque, si indicas keyPath al crear el almacén de objetos, no necesitas escribir el nombre de la clave en cada llamada a put(): basta con pasar el valor.

Esto rellena store0 en cuanto lo hemos creado:

javascript
(async () => {
  //...
  const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(dbName, version,{
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
      store.put('Hello world!', 'Hello')
    }
  })
})()

Añadir datos a un almacén ya creado, mediante transacciones

Para añadir elementos más adelante tienes que crear una transacción de lectura/escritura que garantice la integridad de la base de datos (si una operación falla, todas las operaciones de la transacción se anulan y el estado vuelve al inicial).

Para ello, usa una referencia al objeto dbPromise que hemos obtenido al llamar a openDB, y lanza:

javascript
(async () => {
  //...
  const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(/* ... */)

const tx = db.transaction(storeName, 'readwrite')
  const store = await tx.objectStore(storeName)

const val="hey!"
  const key = 'Hello again'
  const value = await store.put(val, key)
  await tx.done
})()

Obtener datos de un almacén

Obtener un elemento de un almacén de objetos: get()

javascript
const key = 'Hello again'
const item = await db.transaction(storeName).objectStore(storeName).get(key)

Obtener todos los elementos de un almacén de objetos: getAll()

Obtener todas las claves guardadas

javascript
const items = await db.transaction(storeName).objectStore(storeName).getAllKeys()

Obtener todos los valores guardados

javascript
const items = await db.transaction(storeName).objectStore(storeName).getAll()

Eliminar datos de IndexedDB

Borrado de la base de datos, de un almacén de objetos y de los datos

Eliminar por completo una base de datos IndexedDB

javascript
const dbName="mydbname"
await deleteDB(dbName)

Para eliminar datos de un almacén de objetos

Usamos una transacción:

javascript
(async () => {
  //...

const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
    }
  })

const tx = await db.transaction(storeName, 'readwrite')
  const store = await tx.objectStore(storeName)

const key = 'Hello again'
  await store.delete(key)
  await tx.done
})()

Migrar desde una versión anterior de la base de datos

El tercer parámetro (opcional) de la función openDB() es un objeto que puede contener una función de actualización que solo se llama si el número de versión es superior al de la base de datos instalada. En el cuerpo de esa función puedes actualizar la estructura (almacenes e índices) de la base de datos:

javascript
const name="mydbname"
const version = 1
openDB(name, version, {
  upgrade(db, oldVersion, newVersion, transaction) {
    console.log(oldVersion)
  }
})

En ese callback puedes comprobar desde qué versión actualiza el usuario y ejecutar unas operaciones u otras en consecuencia.

Puedes migrar desde una versión anterior de la base de datos con esta sintaxis

javascript
(async () => {
  //...
  const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      switch (oldVersion) {
        case 0:
          // un almacén introducido en la versión 1
          db.createObjectStore('store1')
        case 1:
          // eliminar el almacén antiguo en la versión 2 y crear uno nuevo
          db.createObjectStore('store2', { keyPath: 'name' })
      }
      db.createObjectStore(storeName)
    }
  })
})()

Clave única

createObjectStore(), como ves en el caso 1, acepta un segundo parámetro que indica la clave de índice de la base de datos. Resulta muy útil cuando guardas objetos: las llamadas a put() no necesitan un segundo parámetro, basta con pasar el valor (un objeto) y la clave se asociará a la propiedad del objeto que lleve ese nombre.

El índice te da una forma de recuperar más adelante un valor por esa clave concreta, y debe ser único (cada elemento tiene que tener una clave distinta).

Una clave se puede configurar con autoincremento, así no tienes que gestionarla en el código cliente:

javascript
db.createObjectStore('notes', { autoIncrement: true })

Usa el autoincremento si tus valores no contienen ya una clave única.


Comprobar si existe un almacén de objetos

Puedes comprobar si un almacén de objetos ya existe llamando al método objectStoreNames():

javascript
const storeName="store1"

if (!db.objectStoreNames.contains(storeName)) {
  db.createObjectStore(storeName)
}

Eliminar de IndexedDB

Borrado de la base de datos, de un almacén de objetos y de los datos

Eliminar una base de datos

javascript
await deleteDB('mydb')

Eliminar un almacén de objetos

Un almacén de objetos solo se puede eliminar en el callback de apertura de una base de datos, y ese callback solo se llama si indicas una versión superior a la que ya está instalada:

javascript
const db = await openDB('dogsdb', 2, {
  upgrade(db, oldVersion, newVersion, transaction) {
    switch (oldVersion) {
      case 0:
        // un almacén introducido en la versión 1
        db.createObjectStore('store1')
      case 1:
        // eliminar el almacén antiguo en la versión 2 y crear uno nuevo
        db.deleteObjectStore('store1')
        db.createObjectStore('store2')
    }
  }
})

Para eliminar datos de un almacén de objetos, usa una transacción

javascript
const key = 232

const db = await openDB(/*...*/)
const tx = await db.transaction('store', 'readwrite')
const store = await tx.objectStore('store')
await store.delete(key)
await tx.complete

JavaScript

Damien Flandrin Desarrollador web desde 2010, creador de Gekkode y de Email Impact. Cada artículo se prueba en un proyecto real antes de publicarse. Contacto
Newsletter

Las nuevas pruebas, tutoriales y proyectos, por correo.

Pruebas reproducibles, código versionado, resultados fechados. Nunca spam.