Para el manejo de datos en una PWA de manera normal y previendo su funcionamiento cuando no hay conexión, es interesante trabajar con una base de datos local en formato indexedDB que se sincroniza con una base de datos remota al iniciar la aplicación, para prever la perdida de datos por una alteración o borrado de la cache del navegador del dispositivo que es donde se almacenan los datos locales de las PWA.
El funcionamiento de manejo de una base de datos indexada indexedDB tiene una fase de definición, una segunda de inicialización y otra de manejo y actualización.
Yo he creado una clase javascript “Indxdb” para manejar las bases de datos indexedDB de la aplicación. La clase realiza múltiples acciones sobre la base de datos pero trabajando siempre sobre uno de los contenedores que tenga, aunque indexedDB permite en las transacciones trabajar sobre más de un contenedor.
La base de datos tiene que tener un nombre con el que la pongamos identificar, solo afecta al código interno de la aplicación, en la cache pueden haber varias bases de datos con el mismo nombre ya que la cache las identifica de otra manera.
El segundo parámetro esencial es la versión de la base de datos, es un número entero, no se pueden usar decimales 1.5 lo entenderá como 1. El cambio de versión además de identificarnos a nosotros en que versión estamos, dispara el evento onupgradeneeded, permitiéndote hacer cambios y ajustes en la función que lo recoge actualizando la estructura e índices.
Para definir la base de datos creamos una instancia de la clase en el evento onload para saber que esta todo el contenido cargado. Además fuera de la función tenemos que haber creado el objeto que manejará la base de datos para que tenga un ámbito global.
<script>
var idb = null;
var basedat = {
nom: "idbprob",
ver: 1,
inic: 'inicPerson',
conte: [
{
nom:"personas",
key:"id",
auto:true,
index:[
{cod:"name",nom:"name",options:{unique:false}},
{cod:"dni",nom:"dni",options:{unique:true}}
]
}
],
delconte: []
}
window.onload = function() {
// nom,ver,conte,funcfin
idb = new Inxdb(basedat.nom,basedat.ver,basedat.conte,basedat.delconte,basedat.inic);
}
</script>
En el evento onload creamos una instancia de la clase en el objeto idb que hemos dado de alta al principio, pasando los parámetros de inicialización de la base de datos.
// nombre,versión,contenedores,contenedores a borrar,función final idb = new Inxdb(basedat.nom,basedat.ver,basedat.conte,basedat.delconte,basedat.inic);
La clase está creada para el manejo de bases de datos indexedDB. La clase opera sobre un solo contenedor en cada consulta, aunque indexedDB permite hacer una transacción a varios contenedores a la vez.
class Inxdb{
bd = null;
nom = "";
ver = 1;
con = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
constructor(nom="inxdba",ver="1",conte=[],del=[],func="") {
this.nom = nom;
this.ver = ver;
this.bd = this.con.open(nom,ver);
this.bd.onupgradeneeded = function (e) {
let active = e.currentTarget.result;
let object = null;
// creamos los contenedores
for(let c=0; c<conte.length; c++) {
if (!active.objectStoreNames.contains(conte[c].nom)) {
object = active.createObjectStore(conte[c].nom, { keyPath : conte[c].key, autoIncrement : conte[c].auto });
for(let i=0; i<conte[c].index.length; i++) {
object.createIndex(conte[c].index[i].cod, conte[c].index[i].nom, conte[c].index[i].options);
}
}
}
// contenedores a borrar
for(let d=0; d<del.length; d++) {
if (active.objectStoreNames.contains(del[d])) { active.deleteObjectStore(del[d]); }
}
};
this.bd.onerror = function (e) { alert('Error creando la base de datos'); }
this.bd.onsuccess = function (e) {
console.log('Base de datos creada');
// iniciamos el proceso con la base de datos
if(func !== "") { window[func](); }
}
}
listado(acc,con,ind,tipo,rango,dat,func) {
let bd = this.bd.result;
let bdmodo = "readwrite";
if(acc === "load" || acc === "info") { bdmodo = "readonly"; }
let bddata = bd.transaction([con], bdmodo);
let bdprev = bddata.objectStore(con);
let bdobj = bdprev;
if(ind !== "" && acc !== "edd" && acc !== "busc") { bdobj = bdprev.index(ind); }
let bdrequest = null;
let datbusc = dat;
switch(acc) {
default:
// buscar registros por defecto todos
dat = [];
bdobj.openCursor().onsuccess = function (e) {
let result = e.target.result;
if (result === null) { return; }
let valido = true;
if(acc === "busc") {
// solo algunos
let rdat = result.value;
if(rango === "full") {
if(rdat[ind] !== datbusc) { valido = false; }
}else{
if(rdat[ind].indexOf(datbusc) === -1) { valido = false; }
}
}
if(valido) { dat.push(result.value); }
result.continue();
};
break;
case "info": case "edd":
// solo uno
if(acc === "edd") { datbusc = dat[ind]; }
if(tipo === "string") {
bdrequest = bdobj.get(String(datbusc));
}else{
bdrequest = bdobj.get(parseInt(datbusc));
}
break;
case "add": bdrequest = bdobj.put(dat); break;
case "del": bdrequest = bdobj.delete(dat); break;
}
if(bdrequest !== null) {
bdrequest.onerror = function (e) {
alert(bdrequest.error.name + '\n\n' + bdrequest.error.message);
};
bdrequest.onsuccess = function(e) {
if(acc === "edd") {
// actualizamos el objeto
let bdrequestUpdate = bdobj.put(dat);
bdrequestUpdate.onerror = function(event) { console.log("Error actualizando objeto"); };
bdrequestUpdate.onsuccess = function(event) { console.log("Actualizado objeto"); };
}
}
}
bddata.oncomplete = function (e) {
switch(acc) {
case "info": dat = bdrequest.result; break;
}
if(func !== "") { window[func]({acc:acc,con:con,dat:dat}); }
};
}
}
La base de datos se inicia con el constructor usando la función open indicándole el nombre y la versión, si hay un cambio de versión o la base de datos no se había creado previamente se dispara el evento onupgradeneeded y cuando todo termina se dispara el evento onsuccess en el que incluimos la llamada a la función inic si no esta vacía. Si hubiera algún error se dispararía el evento onerror.
En el evento onupgradeneeded creamos los contenedores en los que se organizan los datos de la indexedDB, no es un sistema de tablas y registros, es un sistema de contenedores de objetos, creamos un contenedor y lo rellenamos de objetos. Los contenedores son los objectStore de la base de datos indexedDB.
Cada contenedor tiene una clave por la que se manejan los objetos que recibirá para poder identificarlos, esa propiedad la tienen que tener todos los objetos que se añadan y tiene que ser única. Para definir esa característica en el contenedor tenemos las propiedades del array de contenedores (key y auto), según estén rellenos se comportará el contenedor:
| Clave (Key-Path) | AutoIncrement | Descripción |
| SI | NO | Se rellena con objetos javascript y estos deben de tener una propiedad con el nombre de la clave. |
| SI | SI | Se rellena con objetos javascript y no necesitan tener una propiedad con el nombre de la clave ya que se genera de manera automática. Si dicha propiedad ya existe en el objeto, el valor de esa propiedad es usado como clave en lugar de generar una nueva. |
Cuando una vez creada una base de datos queremos añadir o eliminar un contenedor, hacemos cambios en el objeto que define la base de datos y aumentamos el numero de la versión para que ejecute de nuevo el evento onupgradeneeded.
Para manejar los datos nos movemos por los distintos contenedores consultando, añadiendo, editando o eliminando objetos. Para ello usamos la función listado de la clase ala que le pasamos:
Para realizar acciones en la indexedDB usamos transacciones, una transacción la podemos abrir para una sola acción o para varias a la vez, en principio el método que yo utilizo y muestro aquí está preparado para realizar acciones por separado. La transacción cuando se declara pasa dos parámetros, un array de contenedores (yo en la función le paso solo un elemento en el array) y el modo de trabajo que puede ser solo lectura «readonly» o lectura y escritura «readwrite«.
Las acciones que se pueden realizar invocando el método listado de la clase son:
Bien todos o solo los que coincidan con un parámetro. En la función identificamos la acción a realizar: load (listado de todos los objetos de un contenedor), info (datos de un objeto), busc (búsqueda completa o parcial de un texto en un contenedor), le pasamos index y tipo así como el dato a buscar en dat si es solo un objeto, si no lo que hace es ordenar el resultado por el índice que le pasemos. Si le pasamos ind vacío usará la clave por defecto del contenedor para ordenar o filtrar.
// acción, contenedor, index, tipo, rango, dato, función fin
idb.listado('busc','personas','dni','string','','22','personlist');
Añadir objetos pasando un objeto añadir dentro del parámetro dat. La ación que pasamos es add.
// acción, contenedor, index, tipo, rango, dato, función fin
idb.listado('add','personas','','','',{nom:"paco",dni:"0000000G"},'personlist');
La acción la identificamos con el código edd. Tenemos que pasarle en ind la propiedad por la que se va a buscar el objeto, tiene que ser un valor único. Y en dat el objeto con sus datos para sustituir a los que tiene.
// acción, contenedor, index, tipo, rango, dato, función fin
idb.listado('edd','personas','','','',{nom:"paco",dni:"7770000G"},'personlist');
La acción la identificamos con el código del. Tenemos que pasar en dat el valor de la clave del contenedor que identifica al objeto a elimina, usamos la clave por defecto del contenedor.
// acción, contenedor, index, tipo, rango, dato, función fin
idb.listado('del','personas','','','',3,'personlist');