Con esto de hacer aplicaciones web cada vez más «potentes», es fácil encontrarse escenarios en los que necesitamos almacenar información localmente, por ejemplo para ganar en rendimiento o trabajar sin conexión. Es discutible si esto es una buena idea o no, a fin de cuentas hay tecnologías más apropiadas y consolidadas para estos casos, pero el hecho es que es posible y en este post vamos a ver una de las formas recomendadas de almacenar localmente informmación en una aplicación web: usando IndexedDB.
OJO: como viene siendo habitual, esto no va a ser un tutorial lleno de código y explicaciones paso a paso, para eso puedes revisar la documentación de Mozilla sobre IndexedDB. Como diría alguno que conozco, que tiene la extraña costumbre de antropomorfizar librerías, el objetivo es ponerle cara y ojos a IndexedDB, como en su día hicimos con AngularJS o ReactJS.
Qué es IndexedDB
IndexedDB es una base de datos que nos permite almacenar información localmente en un navegador y que está soportada por todos los navegadores modernos (incluso Internet Explorer).
Por suerte o por desgracia, no es una base de datos relacional clásica como era WebSQL (ahora descontinuada), sino que se trata de una base de datos clave/valor.
El punto de entrada al API de IndexedDB es el objeto window.indexedDB
, que nos permite crear, eliminar y actualizar bases de datos. Es algo que me ha llamado la atención, porque en el propio API han tenido en cuenta el versionado de bases de datos, algo fundamental cuando llegamos al mundo real.
Todo el API es asíncrono lo que, como te puedes imaginar, en el caso de javascript se traduce en callbacks y eventos por todas partes. Nada nuevo (ni, desde mi punto de vista, especialmente agradable). Cada operación que hacemos nos devuelve un objeto Request
al que podemos enganchar manejadores de eventos tipo onsuccess
, onerror
, etc.
Por ejemplo, para crear una base de datos necesitamos un código parecido a éste:
var db; var openRequest = indexedDB.open('dbName', 1 /* versión de base de datos */); openRequest.onsuccess = function(e) { // Almacenamos una referencia global a la base de datos que acabamos de abrir db = e.target.result; }; openRequest.onupgradeneeded = function(e) { // La base de datos no existía o su versión era inferior a la solicitada, // podemos proceder a actualizarla, crear los objectStores que queramos, etc. // ... } openRequest.onerror = function(e)) { // No hemos podido abrir la base de datos }
ObjectStores e índices
La base de datos se estructura alrededor del concepto de objectStores
, que podríamos ver como algo similar a tablas en SQL Server o colecciones en MongoDB. Por supuesto, esto es Javascript y nosotros somos gente moderna, por lo que lo que guardamos en cada objectStore
no necesita tener ningún esquema definido a priori y podemos ir almacenando más o menos lo que nos dé la gana.
Los objectStores
deben crearse en el evento onupgradeneeded
que se lanza al abrir la base de datos. Esto ayuda bastante a mantener una política de versionando y migraciones de base de datos consistente. Puesto que debo crearlo en ese evento, necesito incrementar la versión de la base de datos para que se lance el evento, lo que permite tener cierta trazabilidad entre versiones de la base de datos.
Cada objeto que almacenamos en un objectStore
tiene una propiedad que hace de clave y nos permite recuperar el objeto para trabajar con él, modificarlo, o eliminarlo. La clave se define en el momento de crear el objectStore
y puede tener distintos tipos de datos (number
, string
, date
o array
). Las claves pueden ser generadas automáticamente por IndexedDB (típica clave autoincremental), ser leídas de una propiedad del objeto que estamos usando como valor, o ser indicadas explícitamente en cada operación de lectura/escritura.
Como buscar objetos a partir de su clave está bien pero es bastante limitado, en IndexedDB podemos definir índices que nos permite buscar objetos por otros campos. Existen varias opciones a la hora de crear un índice, como si es único o no, o la forma de tratar valores duplicados, pero no nos vamos a detener mucho en ellas por ahora.
Crear un objectStore
y añadirle un par de índices no es complicado:
openRequest.onupgradeneeded = function(e) { db = e.target.result; // Creamos un objectStore llamado "people" para guardarobjetos // cuya clave estará en la propiedad "id" y será generada por // indexedDB con un valor autoincremental var storeRequest = db.createObjectStore('people', { keyPath: 'id', autoIncrement: 'true' }); // Creamos un índice único sobre el email de cada persona storeRequest.createIndex('idx_email', 'email', {unique: true}); // Creamos otro índice sobre la ciudad: storeRequest.createIndex('idx_city', 'city', {unique: false}); storeRequest.transaction.oncomplete = function(e) { // Todo es asíncrono, incluida la creación de objectStores // e índices. Cuando se complete la transacción sabremos // que ha terminado la operación y podemos empezar a trabajar // con el objectStore y el índice }; }
Es importante planificar correctamente los índices que vamos a crear sobre cada objectStore
, porque como veremos en el siguiente post, junto con la clave, son la única forma que tenemos de localizar objetos. Para los que estéis muy acostumbrados a usar bases de datos SQL, en las que se pueden lanzar consultas arbitrarias (con permiso de los DBAs), esto os resultará un poco incómodo, pero la realidad es que al final tampoco estás lanzando consultas por 19 campos distintos y no es tan complicado.
Resumen
Esta pequeña introducción nos ha permitido hacernos una idea de los conceptos fundamentales que se esconden detrás de IndexedDB y en próximos posts veremos cómo trabajar con ella para hacer operaciones básicas.
Como decía al principio del post, no estoy muy convencido de que sea buena idea llevar tan lejos el uso de aplicaciones HTML, pero si después de analizar pros y contras decides que quieres implementar una aplicación HTML que requiere tener una base de datos «de verdad» en cliente, IndexedDB es la opción más razonable ahora mismo teniendo en cuenta que WebSQL está descontinuada y que cosas como localStorage
son mucho menos flexibles.
Hace unos años me toco desarrollar una web desconectada (para prueba de concepto) y me acuerdo de haber utilizado mucho websql. Si mal no recuerdo, la mayoría de los navegadores lo implementaba a través de sqlite y, a todas vistas, parecía una opción lógica para darle potencia a las web desconectadas para que se comportaran como una app (un poco de lo que promete html5). No entiendo porque la discontinuación de websql y ofrecer “esto” como su reemplazo desaprovechando la experiencia de los programadores con sql (si el problema era sqlite se hubiera desarrollado un nuevo motor pero manteniendo sql para las consultas)
Sobre tu comentario del principio, hablas de elegir otras tecnologías para trabajar en el cliente. A que te refieres? Porque no te gusta la web como opción en el cliente?
Saludos
No tengo ni idea de por qué mataron WebSQL, pero me extrañó tanto como a ti.
Yo estuve jugando con WebSQL con PhoneGap y me pareció muy cómodo y mucho más ajustado a lo que la mayoría de la gente suele utilizar. Es verdad que en javascript puede resultar cómodo guardar los objetos «tal cual» en un almacenamiento clave/valor como indexedDB, pero obliga a pensar de forma diferente.
Sobre lo del trabajar en el cliente, no es que no me guste la tecnología web. De hecho, es más bien al contrario, si puedo prefiero usar «aplicaciones» web completas en cliente por motivos de portabilidad, despliegue, etc. Aun así, no deja de ser forzar una tecnología que originalmente estaba diseñada para otros escenarios, y eso a veces se nota.
SQLite es realmente potente. Lástima no hayan dado soporte.
Lo que veo de los manejadores de eventos tipo onsuccess, onerror, es el problema para «esperar» si cierta operación acaba en success o error.
Creo recordar que había una librería js, Promises, que iba por ese camino.
Saludos.