Trabajar con IndexedDB
Spanish (Español) translation by Juan Pablo Diaz Cuartas (you can also view the original English article)
Uno de los desarrollos más interesantes en los estándares web últimamente es la especificación de la base de datos indexada (IndexedDB para abreviar). Para pasar un rato divertido, puede leer la especificación usted mismo. En este tutorial explicaré esta característica y espero que te dé un poco de inspiración para usar esta poderosa función por ti mismo.
Visión de conjunto
Como una especificación, IndexedDB es actualmente una Recomendación Candidata.
En pocas palabras, IndexedDB proporciona una forma de almacenar grandes cantidades de datos en el navegador de su usuario. Cualquier aplicación que necesite enviar una gran cantidad de datos a través del cable podría beneficiarse enormemente de poder almacenar esos datos en el cliente. Por supuesto, el almacenamiento es solo una parte de la ecuación. IndexedDB también proporciona una poderosa API de búsqueda basada en índices para recuperar los datos que necesita.
¿Te preguntas cómo IndexedDB difiere de otros mecanismos de almacenamiento?
Las cookies son extremadamente compatibles, pero tienen implicaciones legales y un espacio de almacenamiento limitado. Además, se envían y reciben en el servidor con cada solicitud, anulando por completo los beneficios del almacenamiento en el lado del cliente.
El almacenamiento local también es muy compatible, pero limitado en términos de la cantidad total de almacenamiento que puede usar. El almacenamiento local no proporciona una verdadera API de "búsqueda" ya que los datos solo se recuperan a través de valores clave. El almacenamiento local es ideal para cosas "específicas" que desee almacenar, por ejemplo, preferencias, mientras que IndexedDB es más adecuado para datos Ad Hoc (muy parecido a una base de datos).
Sin embargo, antes de seguir adelante, hablemos honestamente sobre el estado de IndexedDB en términos de compatibilidad con el navegador. Como una especificación, IndexedDB es actualmente una Recomendación Candidata. En este punto, las personas detrás de la especificación están felices con él, pero ahora están buscando comentarios de la comunidad de desarrolladores. La especificación puede cambiar de ahora a la etapa final, Recomendación W3C. En general, los navegadores compatibles con IndexedDB ahora todos lo hacen de una manera bastante consistente, pero los desarrolladores deben estar preparados para tratar con prefijos y tomar nota de las actualizaciones en el futuro.
En cuanto a los navegadores compatibles con IndexedDB, tiene un pequeño dilema. El soporte es bastante bueno para el escritorio, pero prácticamente inexistente para el móvil. Veamos cuál es el excelente sitio que CanIUse.com dice:



Chrome para Android admite la función, pero muy pocas personas están usando ese navegador en dispositivos Android. ¿La falta de soporte móvil implica que no deberías usarlo? ¡Por supuesto que no! Esperamos que todos nuestros lectores estén familiarizados con el concepto de mejora progresiva. Las características como IndexedDB se pueden agregar a su aplicación de una manera que no se rompa en navegadores no compatibles. Puede usar las bibliotecas contenedoras para cambiar a WebSQL en dispositivos móviles, o simplemente omitir el almacenamiento local de datos en sus clientes móviles. Personalmente, creo que la capacidad de almacenar en caché grandes bloques de datos en el cliente es lo suficientemente importante como para usarla ahora incluso sin soporte móvil.
Empecemos
Cubrimos las especificaciones y el soporte, ahora veamos cómo usar la función. Lo primero que debemos hacer es verificar si hay soporte para IndexedDB. Si bien existen herramientas que brindan formas genéricas para verificar las características del navegador, podemos simplificar esto, ya que solo estamos buscando una cosa en particular.
1 |
document.addEventListener("DOMContentLoaded", function(){ |
2 |
|
3 |
if("indexedDB" in window) { |
4 |
console.log("YES!!! I CAN DO IT!!! WOOT!!!"); |
5 |
} else { |
6 |
console.log("I has a sad."); |
7 |
}
|
8 |
|
9 |
},false); |
El fragmento de código anterior (disponible en test1.html si descarga el archivo zip adjunto a este artículo) utiliza el evento DOMContentLoaded para esperar a que se cargue la página. (Bueno, eso es obvio, pero reconozco que esto puede no ser familiar para las personas que solo han usado jQuery). Luego, simplemente veo si indexedDB existe en el objeto ventana y, de ser así, estamos listos para continuar. Ese es el ejemplo más simple, pero normalmente probablemente querríamos almacenar esto para saber más adelante si podemos usar la característica. Aquí hay un ejemplo un poco más avanzado (test2.html).
1 |
var idbSupported = false; |
2 |
|
3 |
document.addEventListener("DOMContentLoaded", function(){ |
4 |
|
5 |
if("indexedDB" in window) { |
6 |
idbSupported = true; |
7 |
}
|
8 |
|
9 |
},false); |
Todo lo que hice fue crear una variable global, idbSupported, que se puede usar como indicador para ver si el navegador actual puede usar IndexedDB.
Abrir una base de datos
IndexedDB, como se puede imaginar, hace uso de bases de datos. Para ser claros, esta no es una implementación de SQL Server. Esta base de datos es local para el navegador y solo está disponible para el usuario. Las bases de datos IndexedDB siguen las mismas reglas que las cookies y el almacenamiento local. Una base de datos es exclusiva del dominio desde el que se cargó. Entonces, por ejemplo, una base de datos llamada "Foo" creada en foo.com no entrará en conflicto con una base de datos con el mismo nombre en goo.com. No solo no entrará en conflicto, sino que tampoco estará disponible para otros dominios. Puede almacenar datos para su sitio web sabiendo que otro sitio web no podrá acceder a él.
La apertura de una base de datos se realiza mediante el comando abrir. En el uso básico, proporciona un nombre y una versión. La versión es muy importante por razones que cubriré más adelante. Aquí hay un ejemplo simple:
1 |
var openRequest = indexedDB.open("test",1); |
Abrir una base de datos es una operación asincrónica. Para manejar el resultado de esta operación, deberá agregar algunos detectores de eventos. Hay cuatro tipos diferentes de eventos que se pueden disparar:
- éxito
- error
- upgradeneeded
- obstruido
Probablemente puedas adivinar qué implican el éxito y el error. El evento de actualización se usa tanto cuando el usuario abre la base de datos por primera vez como cuando cambia la versión. Bloqueado no es algo que sucederá normalmente, pero puede disparar si una conexión previa nunca se cerró.
Normalmente, lo que debería suceder es que, en el primer acceso a su sitio, se activará el evento de actualización. Después de eso, solo el controlador de éxito. Veamos un ejemplo simple (test3.html).
1 |
var idbSupported = false; |
2 |
var db; |
3 |
|
4 |
document.addEventListener("DOMContentLoaded", function(){ |
5 |
|
6 |
if("indexedDB" in window) { |
7 |
idbSupported = true; |
8 |
}
|
9 |
|
10 |
if(idbSupported) { |
11 |
var openRequest = indexedDB.open("test",1); |
12 |
|
13 |
openRequest.onupgradeneeded = function(e) { |
14 |
console.log("Upgrading..."); |
15 |
}
|
16 |
|
17 |
openRequest.onsuccess = function(e) { |
18 |
console.log("Success!"); |
19 |
db = e.target.result; |
20 |
}
|
21 |
|
22 |
openRequest.onerror = function(e) { |
23 |
console.log("Error"); |
24 |
console.dir(e); |
25 |
}
|
26 |
|
27 |
}
|
28 |
|
29 |
},false); |
Una vez más, verificamos si IndexedDB en realidad es compatible, y si lo es, abrimos una base de datos. Hemos cubierto tres eventos aquí: el evento de actualización necesaria, el evento de éxito y el evento de error. Por ahora concéntrese en el evento de éxito. El evento pasa un controlador a través de target.result. Hemos copiado eso a una variable global llamada db. Esto es algo que usaremos más tarde para agregar datos. Si ejecuta esto en su navegador (¡en uno que sea compatible con IndexedDB, por supuesto!), Debería ver el mensaje de actualización y éxito en su consola la primera vez que ejecuta el script. La segunda, y así sucesivamente, veces que ejecuta el script, solo debería ver el mensaje de éxito.
Tiendas de objetos
Hasta ahora, hemos comprobado el soporte de IndexedDB, lo hemos confirmado y hemos abierto una conexión a una base de datos. Ahora necesitamos un lugar para almacenar datos. IndexedDB tiene un concepto de "Tiendas de objetos". Puede pensar en esto como una tabla de base de datos típica. (Es mucho más flexible que una tabla de base de datos típica, pero no se preocupe por eso ahora). Los almacenes de objetos tienen datos (obviamente) pero también un keypath y un conjunto opcional de índices. Keypaths son básicamente identificadores únicos para sus datos y vienen en diferentes formatos. Los índices se tratarán más adelante cuando comencemos a hablar sobre la recuperación de datos.
Ahora para algo crucial. ¿Recuerdas el evento de actualización mencionado anteriormente? Solo puede crear almacenes de objetos durante un evento de actualización. Ahora, de manera predeterminada, se ejecutará automáticamente la primera vez que un usuario acceda a su sitio. Puede usar esto para crear sus almacenes de objetos. Lo más importante que debes recordar es que si alguna vez necesitas modificar tus almacenes de objetos, vas a necesitar actualizar la versión (en ese evento abierto) y escribir código para manejar tus cambios. Veamos un ejemplo simple de esto en acción.
1 |
var idbSupported = false; |
2 |
var db; |
3 |
|
4 |
document.addEventListener("DOMContentLoaded", function(){ |
5 |
|
6 |
if("indexedDB" in window) { |
7 |
idbSupported = true; |
8 |
}
|
9 |
|
10 |
if(idbSupported) { |
11 |
var openRequest = indexedDB.open("test_v2",1); |
12 |
|
13 |
openRequest.onupgradeneeded = function(e) { |
14 |
console.log("running onupgradeneeded"); |
15 |
var thisDB = e.target.result; |
16 |
|
17 |
if(!thisDB.objectStoreNames.contains("firstOS")) { |
18 |
thisDB.createObjectStore("firstOS"); |
19 |
}
|
20 |
|
21 |
}
|
22 |
|
23 |
openRequest.onsuccess = function(e) { |
24 |
console.log("Success!"); |
25 |
db = e.target.result; |
26 |
}
|
27 |
|
28 |
openRequest.onerror = function(e) { |
29 |
console.log("Error"); |
30 |
console.dir(e); |
31 |
}
|
32 |
|
33 |
}
|
34 |
|
35 |
},false); |
Este ejemplo (test4.html) se basa en las entradas anteriores, así que me centraré en las novedades. Dentro del evento de actualización necesitado, hice uso de la variable de base de datos que se le pasó (thisDB). Una de las propiedades de esta variable es una lista de almacenes de objetos existentes llamados objectStoreNames. Para la gente curiosa, esta no es una matriz simple sino una "DOMStringList". No me preguntes, pero ya estás. Podemos usar el método contains para ver si nuestro almacén de objetos existe, y si no, crearlo. Esta es una de las pocas funciones sincrónicas en IndexedDB, por lo que no tenemos que escuchar el resultado.
Para resumir, esto es lo que sucedería cuando un usuario visite su sitio. La primera vez que están aquí, se activa el evento de actualización. El código verifica si existe un almacén de objetos, "firstOS". No lo hará. Por lo tanto, está creado. Luego se ejecuta el controlador de éxito. La segunda vez que visiten el sitio, el número de versión será el mismo, por lo que no se activará el evento de actualización.
Ahora imagine que quería agregar una segunda tienda de objetos. Todo lo que necesita hacer es incrementar el número de versión y básicamente duplicar el bloque de código contains / createObjectStore que ve arriba. Lo bueno es que su código de actualización será compatible tanto con las personas que son nuevas en el sitio como con aquellas que ya tenían la primera tienda de objetos. Aquí hay un ejemplo de esto (test5.html):
1 |
var openRequest = indexedDB.open("test_v2",2); |
2 |
|
3 |
openRequest.onupgradeneeded = function(e) { |
4 |
console.log("running onupgradeneeded"); |
5 |
var thisDB = e.target.result; |
6 |
|
7 |
if(!thisDB.objectStoreNames.contains("firstOS")) { |
8 |
thisDB.createObjectStore("firstOS"); |
9 |
}
|
10 |
|
11 |
if(!thisDB.objectStoreNames.contains("secondOS")) { |
12 |
thisDB.createObjectStore("secondOS"); |
13 |
}
|
14 |
|
15 |
}
|
Agregar datos
Una vez que tenga listas sus tiendas de objetos, puede comenzar a agregar datos. Este es, tal vez, uno de los aspectos más interesantes de IndexedDB. A diferencia de las bases de datos tradicionales basadas en tablas, IndexedDB le permite almacenar un objeto tal cual. Lo que eso significa es que puede tomar un objeto JavaScript genérico y simplemente almacenarlo. Hecho. Obviamente, hay algunas advertencias aquí, pero en su mayor parte, eso es todo.
Trabajar con datos requiere que uses una transacción. Las transacciones toman dos argumentos. El primero es una matriz de tablas con las que trabajarás. La mayoría de las veces esta será una mesa. El segundo argumento es el tipo de transacción. Hay dos tipos de transacciones: readonly y readwrite. Agregar datos será una operación de lectura. Comencemos por crear la transacción:
1 |
//Assume db is a database variable opened earlier
|
2 |
var transaction = db.transaction(["people"],"readwrite"); |
Tenga en cuenta que el almacén de objetos, "personas", es uno que hemos creado en el ejemplo anterior. Nuestra próxima demostración completa hará uso de ella. Después de obtener la transacción, luego solicite la tienda de objetos con la que dijo que trabajaría:
1 |
var store = transaction.objectStore("people"); |
Ahora que tienes la tienda, puedes agregar datos. Esto se hace mediante el método - aguardarlo - agregar.
1 |
//Define a person
|
2 |
var person = { |
3 |
name:name, |
4 |
email:email, |
5 |
created:new Date() |
6 |
}
|
7 |
|
8 |
//Perform the add
|
9 |
var request = store.add(person,1); |
Recuerde que antes dijimos que puede almacenar cualquier información que desee (en su mayor parte). Entonces mi objeto de persona arriba es completamente arbitrario. Podría haber usado firstName y lastName en lugar de solo name. Podría haber usado una propiedad de género. Entiendes la idea. El segundo argumento es la clave utilizada para identificar de manera única los datos. En este caso, lo codificamos con dificultad en 1, lo que causará un problema con bastante rapidez. Está bien, aprenderemos a corregirlo.
La operación de agregar es ascíncrona, así que vamos a agregar dos manejadores de eventos para el resultado.
1 |
request.onerror = function(e) { |
2 |
console.log("Error",e.target.error.name); |
3 |
//some type of error handler
|
4 |
}
|
5 |
|
6 |
request.onsuccess = function(e) { |
7 |
console.log("Woot! Did it"); |
8 |
}
|
Tenemos un manejador de onerror para errores y éxito para buenos cambios. Bastante obvio, pero veamos un ejemplo completo. Puede encontrar esto en el archivo test6.html.
1 |
<!doctype html>
|
2 |
<html>
|
3 |
<head>
|
4 |
</head>
|
5 |
|
6 |
<body>
|
7 |
|
8 |
<script>
|
9 |
var db; |
10 |
|
11 |
function indexedDBOk() { |
12 |
return "indexedDB" in window; |
13 |
}
|
14 |
|
15 |
document.addEventListener("DOMContentLoaded", function() { |
16 |
|
17 |
//No support? Go in the corner and pout.
|
18 |
if(!indexedDBOk) return; |
19 |
|
20 |
var openRequest = indexedDB.open("idarticle_people",1); |
21 |
|
22 |
openRequest.onupgradeneeded = function(e) { |
23 |
var thisDB = e.target.result; |
24 |
|
25 |
if(!thisDB.objectStoreNames.contains("people")) { |
26 |
thisDB.createObjectStore("people"); |
27 |
}
|
28 |
}
|
29 |
|
30 |
openRequest.onsuccess = function(e) { |
31 |
console.log("running onsuccess"); |
32 |
|
33 |
db = e.target.result; |
34 |
|
35 |
//Listen for add clicks
|
36 |
document.querySelector("#addButton").addEventListener("click", addPerson, false); |
37 |
}
|
38 |
|
39 |
openRequest.onerror = function(e) { |
40 |
//Do something for the error
|
41 |
}
|
42 |
|
43 |
},false); |
44 |
|
45 |
function addPerson(e) { |
46 |
var name = document.querySelector("#name").value; |
47 |
var email = document.querySelector("#email").value; |
48 |
|
49 |
console.log("About to add "+name+"/"+email); |
50 |
|
51 |
var transaction = db.transaction(["people"],"readwrite"); |
52 |
var store = transaction.objectStore("people"); |
53 |
|
54 |
//Define a person
|
55 |
var person = { |
56 |
name:name, |
57 |
email:email, |
58 |
created:new Date() |
59 |
}
|
60 |
|
61 |
//Perform the add
|
62 |
var request = store.add(person,1); |
63 |
|
64 |
request.onerror = function(e) { |
65 |
console.log("Error",e.target.error.name); |
66 |
//some type of error handler
|
67 |
}
|
68 |
|
69 |
request.onsuccess = function(e) { |
70 |
console.log("Woot! Did it"); |
71 |
}
|
72 |
}
|
73 |
</script>
|
74 |
|
75 |
<input type="text" id="name" placeholder="Name"><br/> |
76 |
<input type="email" id="email" placeholder="Email"><br/> |
77 |
<button id="addButton">Add Data</button> |
78 |
|
79 |
</body>
|
80 |
</html>
|
El ejemplo anterior contiene una forma pequeña con un botón para disparar un evento para almacenar los datos en IndexedDB. Ejecute esto en su navegador, agregue algo a los campos del formulario y haga clic en Agregar. Si tiene abiertas las herramientas de desarrollo de su navegador, debería ver algo como esto.



Este es un buen momento para señalar que Chrome tiene un excelente visor para los datos de IndexedDB. Si hace clic en la pestaña Recursos, expanda la sección IndexedDB, puede ver la base de datos creada por esta demostración, así como el objeto que acaba de ingresar.



Por diablos, adelante y presione el botón Agregar Datos nuevamente. Debería ver un error en la consola:



El mensaje de error debe ser una pista. ConstraintError significa que simplemente tratamos de agregar datos con la misma clave que uno que ya existía. Si recuerdas, codificamos esa clave y sabíamos que eso sería un problema. Es hora de hablar de llaves.
Llaves
Las claves son la versión de las claves principales de IndexedDB. Las bases de datos tradicionales pueden tener tablas sin claves, pero cada almacén de objetos debe tener una clave. IndexedDB permite un par de diferentes tipos de claves.
La primera opción es simplemente especificarla usted mismo, como hicimos anteriormente. Podríamos usar la lógica para generar claves únicas.
Su segunda opción es una ruta de acceso clave, donde la clave se basa en una propiedad de los datos en sí. Considere nuestro ejemplo de personas: podríamos usar una dirección de correo electrónico como clave.
Su tercera opción, y en mi opinión, la más simple, es usar un generador de claves. Esto funciona de forma similar a una clave primaria autonumber y es el método más simple de especificar claves.
Las claves se definen cuando se crean almacenes de objetos. Aquí hay dos ejemplos: uno que usa una ruta clave y otro un generador.
1 |
thisDb.createObjectStore("test", { keyPath: "email" }); |
2 |
thisDb.createObjectStore("test2", { autoIncrement: true }); |
Podemos modificar nuestra demostración previa creando un almacén de objetos con una clave de autoIncrement:
1 |
thisDB.createObjectStore("people", {autoIncrement:true}); |
Finalmente, podemos tomar la llamada Agregar que usamos antes y eliminar la clave codificada:
1 |
var request = store.add(person); |
¡Eso es! Ahora puede agregar datos todo el día. Puede encontrar esta versión en test7.html.
Lectura de datos
Ahora cambiemos a la lectura de datos individuales (vamos a cubrir la lectura de conjuntos de datos más grandes más adelante). Una vez más, esto se hará en una transacción y será asincrónico. Aquí hay un ejemplo simple:
1 |
var transaction = db.transaction(["test"], "readonly"); |
2 |
var objectStore = transaction.objectStore("test"); |
3 |
|
4 |
//x is some value
|
5 |
var ob = objectStore.get(x); |
6 |
|
7 |
ob.onsuccess = function(e) { |
8 |
|
9 |
}
|
Tenga en cuenta que la transacción es de solo lectura. La llamada a API es solo una simple llamada de obtención con la clave ingresada. Como un aparte rápido, si piensa que usar IndexedDB es un poco detallado, tenga en cuenta que puede encadenar muchas de esas llamadas también. Aquí está exactamente el mismo código escrito mucho más apretado:
1 |
db.transaction(["test"], "readonly").objectStore("test").get(X).onsuccess = function(e) {} |
Personalmente, todavía encuentro IndexedDB un poco complejo, por lo que prefiero el enfoque 'roto' para ayudarme a hacer un seguimiento de lo que está sucediendo.
El resultado del manejador de gets on success es el objeto que almacenaste antes. Una vez que tienes ese objeto, puedes hacer lo que quieras. En nuestra próxima demostración (test8.html) agregamos un campo de formulario simple para permitirle ingresar una clave e imprimir el resultado. Aquí hay un ejemplo:



El controlador para el botón Obtener datos está a continuación:
1 |
function getPerson(e) { |
2 |
var key = document.querySelector("#key").value; |
3 |
if(key === "" || isNaN(key)) return; |
4 |
|
5 |
var transaction = db.transaction(["people"],"readonly"); |
6 |
var store = transaction.objectStore("people"); |
7 |
|
8 |
var request = store.get(Number(key)); |
9 |
|
10 |
request.onsuccess = function(e) { |
11 |
|
12 |
var result = e.target.result; |
13 |
console.dir(result); |
14 |
if(result) { |
15 |
var s = "<h2>Key "+key+"</h2><p>"; |
16 |
for(var field in result) { |
17 |
s+= field+"="+result[field]+"<br/>"; |
18 |
}
|
19 |
document.querySelector("#status").innerHTML = s; |
20 |
} else { |
21 |
document.querySelector("#status").innerHTML = "<h2>No match</h2>"; |
22 |
}
|
23 |
}
|
24 |
}
|
En su mayor parte, esto debería ser auto explicativo. Obtenga el valor del campo y ejecute una llamada a get en la tienda de objetos obtenida de una transacción. Tenga en cuenta que el código de visualización simplemente obtiene todos los campos y los descarta. En una aplicación real, (con suerte) sabrá qué contienen sus datos y trabajará con campos específicos.
Leer más datos
Así es como obtendrías una pieza de información. ¿Qué tal una gran cantidad de datos? IndexedDB tiene soporte para lo que se llama un cursor. Un cursor te permite iterar sobre los datos. Puede crear cursores con un rango opcional (un filtro básico) y una dirección.
Como ejemplo, el siguiente bloque de código abre un cursor para recuperar todos los datos de un almacén de objetos. Como todo lo demás que hemos hecho con los datos, esto es asincrónico y en una transacción.
1 |
var transaction = db.transaction(["test"], "readonly"); |
2 |
var objectStore = transaction.objectStore("test"); |
3 |
|
4 |
var cursor = objectStore.openCursor(); |
5 |
|
6 |
cursor.onsuccess = function(e) { |
7 |
var res = e.target.result; |
8 |
if(res) { |
9 |
console.log("Key", res.key); |
10 |
console.dir("Data", res.value); |
11 |
res.continue(); |
12 |
}
|
13 |
}
|
El controlador de éxito pasa un objeto de resultado (la variable res anterior). Contiene la clave, el objeto para los datos (en la clave de valor anterior) y un método de continuación que se utiliza para iterar a la siguiente pieza de datos.
En la siguiente función, hemos utilizado un cursor para iterar sobre todos los datos del almacén de objetos. Como estamos trabajando con datos de "personas", hemos llamado a esto getPeople:
1 |
function getPeople(e) { |
2 |
|
3 |
var s = ""; |
4 |
|
5 |
db.transaction(["people"], "readonly").objectStore("people").openCursor().onsuccess = function(e) { |
6 |
var cursor = e.target.result; |
7 |
if(cursor) { |
8 |
s += "<h2>Key "+cursor.key+"</h2><p>"; |
9 |
for(var field in cursor.value) { |
10 |
s+= field+"="+cursor.value[field]+"<br/>"; |
11 |
}
|
12 |
s+="</p>"; |
13 |
cursor.continue(); |
14 |
}
|
15 |
document.querySelector("#status2").innerHTML = s; |
16 |
}
|
17 |
}
|
Puede ver una demostración completa de esto en su descarga como archivo test9.html. Tiene una lógica Agregar persona como en los ejemplos anteriores, así que simplemente cree unas pocas personas y luego presione el botón para mostrar todos los datos.



Entonces ahora sabes cómo obtener una pieza de datos y cómo obtener todos los datos. Veamos ahora nuestro tema final: trabajar con índices.
Ellos llaman a este IndexedDB, ¿verdad?
Hemos estado hablando de IndexedDB para todo el artículo, pero aún no hemos hecho ningún - bueno - índices. Los índices son una parte crucial de las tiendas de objetos IndexedDB. Proporcionan una forma de obtener datos en función de su valor, así como de especificar si un valor debe ser único dentro de una tienda. Más adelante demostraremos cómo usar los índices para obtener un rango de datos.
Primero, ¿cómo se crea un índice? Como todo lo demás estructural, deben hacerse en un evento de actualización, básicamente al mismo tiempo que creas tu tienda de objetos. Aquí hay un ejemplo:
1 |
var objectStore = thisDb.createObjectStore("people", |
2 |
{ autoIncrement:true }); |
3 |
//first arg is name of index, second is the path (col);
|
4 |
objectStore.createIndex("name","name", {unique:false}); |
5 |
objectStore.createIndex("email","email", {unique:true}); |
En la primera línea, creamos la tienda. Tomamos ese resultado (un objeto objectStore) y ejecutamos el método createIndex. El primer argumento es el nombre del índice y el segundo es la propiedad que se indexará. En la mayoría de los casos, creo que usarás el mismo nombre para ambos. El argumento final es un conjunto de opciones. Por ahora, solo estamos usando uno, único. El primer índice para el nombre no es único. El segundo para el correo electrónico es. Cuando almacenamos datos, IndexedDB comprobará estos índices y se asegurará de que la propiedad del correo electrónico sea única. También hará un poco de manejo de datos en el back-end para garantizar que podamos obtener datos por estos índices.
¿Cómo funciona? Una vez que busca un almacén de objetos a través de una transacción, puede solicitar un índice de esa tienda. Usando el código de arriba, aquí hay un ejemplo de eso:
1 |
var transaction = db.transaction(["people"],"readonly"); |
2 |
var store = transaction.objectStore("people"); |
3 |
var index = store.index("name"); |
4 |
|
5 |
//name is some value
|
6 |
var request = index.get(name); |
Primero obtenemos la transacción, seguimos por la tienda y luego indexamos. Como hemos dicho antes, podría encadenar esas tres primeras líneas para hacerlo un poco más compacto si lo desea.
Una vez que tenga un índice, puede realizar una llamada de obtención para obtener datos por nombre. Podríamos hacer algo similar para el correo electrónico también. El resultado de esa llamada es otro objeto asíncrono al que puede vincular un controlador onsuccess. Aquí hay un ejemplo de ese controlador encontrado en el archivo test10.html:
1 |
request.onsuccess = function(e) { |
2 |
|
3 |
var result = e.target.result; |
4 |
if(result) { |
5 |
var s = "<h2>Name "+name+"</h2><p>"; |
6 |
for(var field in result) { |
7 |
s+= field+"="+result[field]+"<br/>"; |
8 |
}
|
9 |
document.querySelector("#status").innerHTML = s; |
10 |
} else { |
11 |
document.querySelector("#status").innerHTML = "<h2>No match</h2>"; |
12 |
}
|
13 |
}
|
Tenga en cuenta que una llamada de obtención de índice puede devolver múltiples objetos. Como nuestro nombre no es único, probablemente deberíamos modificar el código para manejarlo, pero no es obligatorio.
Ahora vamos a darle un puntapié. Has visto usar la API get en el índice para obtener un valor basado en esa propiedad. ¿Qué pasa si quieres obtener un conjunto más amplio de datos? El último término que vamos a aprender hoy son Rangos. Los rangos son una forma de seleccionar un subconjunto de un índice. Por ejemplo, dado un índice en una propiedad de nombre, podemos usar un rango para encontrar nombres que comiencen con A hasta nombres que comiencen con C. Los rangos vienen en algunas variedades diferentes. Pueden ser "todo debajo de un marcador", "todo sobre un marcador" y "algo entre un marcador más bajo y un marcador más alto". Finalmente, solo para hacer las cosas interesantes, los rangos pueden ser inclusivos o exclusivos. Básicamente, eso significa que para un rango que va de A-C, podemos especificar si queremos incluir A y C en el rango o solo los valores entre ellos. Finalmente, también puede solicitar rangos ascendentes y descendentes.
Los rangos se crean usando un objeto toplevel llamado IDBKeyRange. Tiene tres métodos de interés: lowerBound, upperBound y bound. lowerBound se utiliza para crear un rango que comienza en un valor inferior y devuelve todos los datos "por encima" de él. UpperBound es lo opuesto. Y, finalmente, bound se usa para soportar un conjunto de datos con un límite inferior y un límite superior. Veamos algunos ejemplos:
1 |
//Values over 39
|
2 |
var oldRange = IDBKeyRange.lowerBound(39); |
3 |
|
4 |
//Values 40a dn over
|
5 |
var oldRange2 = IDBKeyRange.lowerBound(40,true); |
6 |
|
7 |
//39 and smaller...
|
8 |
var youngRange = IDBKeyRange.upperBound(40); |
9 |
|
10 |
//39 and smaller...
|
11 |
var youngRange2 = IDBKeyRange.upperBound(39,true); |
12 |
|
13 |
//not young or old... you can also specify inclusive/exclusive
|
14 |
var okRange = IDBKeyRange.bound(20,40) |
Una vez que tenga un rango, puede pasarlo al método openCursor de un índice. Esto le da un iterador para recorrer los valores que coinciden con ese rango. Como una manera práctica, esto no es realmente una búsqueda per se. Puede usar esto para buscar contenido basado en el comienzo de una cadena, pero no en el medio o el final. Veamos un ejemplo completo. Primero crearemos un formulario simple para buscar personas:
1 |
Starting with: <input type="text" id="nameSearch" placeholder="Name"><br/> |
2 |
Ending with: <input type="text" id="nameSearchEnd" placeholder="Name"><br/> |
3 |
<button id="getButton">Get By Name Range</button> |
Vamos a permitir búsquedas que consistan en cualquiera de los tres tipos de rangos (de nuevo, un valor y más, un valor más alto o los valores dentro de dos entradas). Ahora veamos el controlador de eventos para este formulario.
1 |
function getPeople(e) { |
2 |
var name = document.querySelector("#nameSearch").value; |
3 |
|
4 |
var endname = document.querySelector("#nameSearchEnd").value; |
5 |
|
6 |
if(name == "" && endname == "") return; |
7 |
|
8 |
var transaction = db.transaction(["people"],"readonly"); |
9 |
var store = transaction.objectStore("people"); |
10 |
var index = store.index("name"); |
11 |
|
12 |
//Make the range depending on what type we are doing
|
13 |
var range; |
14 |
if(name != "" && endname != "") { |
15 |
range = IDBKeyRange.bound(name, endname); |
16 |
} else if(name == "") { |
17 |
range = IDBKeyRange.upperBound(endname); |
18 |
} else { |
19 |
range = IDBKeyRange.lowerBound(name); |
20 |
}
|
21 |
|
22 |
var s = ""; |
23 |
|
24 |
index.openCursor(range).onsuccess = function(e) { |
25 |
var cursor = e.target.result; |
26 |
if(cursor) { |
27 |
s += "<h2>Key "+cursor.key+"</h2><p>"; |
28 |
for(var field in cursor.value) { |
29 |
s+= field+"="+cursor.value[field]+"<br/>"; |
30 |
}
|
31 |
s+="</p>"; |
32 |
cursor.continue(); |
33 |
}
|
34 |
document.querySelector("#status").innerHTML = s; |
35 |
}
|
36 |
|
37 |
}
|
De arriba a abajo: comenzamos agarrando los dos campos de formulario. Luego creamos una transacción y de eso obtenemos la tienda y el índice. Ahora para la parte semi-compleja. Dado que tenemos tres tipos diferentes de rangos que necesitamos apoyar, tenemos que hacer un poco de lógica condicional para descubrir qué necesitaremos. El rango que creamos se basa en los campos que completa. Lo bueno es que una vez que tenemos el rango, simplemente lo pasamos al índice y abrimos el cursor. ¡Eso es! Puede encontrar este ejemplo completo en test11.html. Asegúrese de ingresar algunos valores primero para que tenga datos para buscar.
¿Que sigue?
Lo creas o no, solo hemos comenzado nuestra discusión en IndexedDB. En el siguiente artículo, abordaremos temas adicionales, incluidas las actualizaciones y eliminaciones, los valores basados en matrices y algunos consejos generales para trabajar con IndexedDB.



