Friday, April 8, 2016

HTML5 - IndexedDB

IndexedDB

INTRODUCTION

IndexedDB logo
IndexedDB is presented as an alternative to the WebSQL Database, which the W3C deprecated on November 18, 2010 (while still available in some browsers, it is no longer in the HTML5 specification). Both are solutions for storage, but they do not offer the same functionalities. WebSQL Database is a relational database access system, whereasIndexedDB is an indexed table system.
From the W3C specification about IndexedDB: "User agents (apps running in browsers) may need to store large numbers of objects locally in order to satisfy off-line data requirements of Web applications. Where WebStorage (as seen in the HTML5 part 1 course -localStorage and sessionStorage) is useful for storing pairs of keys and their corresponding values, IndexedDB provides in-order retrieval of keys, efficient searching of values, and the storage of duplicate values for a key". 
The W3C specification provides a concrete API to perform advanced key-value data management that is at the heart of most sophisticated query processors. It does so by using transactional databases to store keys and their corresponding values (one or more per key), and providing a means of traversing keys in a deterministic order. This is often implemented through the use of persistent B-tree data structures that are considered efficient for insertion and deletion, as well as in-order traversal of very large numbers of data records.
To sum up:
    1. IndexedDB is a transactional Object Store in which you will be able to store JavaScript objects.
    2. Put indexes on some properties of these objects for faster retrieval and search.
    3. Applications that use IndexedDB can work both online and offline.
    4. IndexedDB is transactional: it manages concurrent access to data.
Examples of applications where IndexedDB should be considered:
google drive uses indexedDB
    • A catalog of DVDs in a lending library.
    • Mail clients, to-do lists, notepads.
    • Data for games: hi scores, level definitions, etc.
    • Google Drive uses IndexedDB extensively...

EXTERNAL RESOURCES

Much of this chapter either builds on or is an adaptation of the content of the following articles posted on the Mozilla Developer Network site (articles are: IndexedDBBasic Concepts of IndexedDB and Using IndexedDB).

CURRENT SUPPORT

Current support is excellent both on mobile and desktop browsers.
IndexedDB support

IndexedDB: basic concepts

INTRODUCTION

IndexedDB is very different from SQL databases, but don't be afraid if you've only used SQL databases: IndexedDB might seem complex at first sight, but it really isn't.
Let's quickly look at the main concepts of IndexedDB, as we will go into detail later on:
    • IndexedDB stores and retrieves objects which are indexed by a "key".
    • Changes to the database happen within transactions.
    • IndexedDB follows a same-origin policy. So while you can access stored data within a domain, you cannot access data across different domains.
    • It makes extensive use of an asynchronous API: most processing will be done in callback functions - and we mean LOTS of callback functions!

DETAILED OVERVIEW

IndexedDB databases store key-value pairs. The values can be complex structured objects (hint: imagine JSON objects), and keys can be properties of those objects. You can create indexes that use any property of the objects for quick searching, as well as sorted enumeration.
Example of data (we reuse some sample from this MDN tutorial):
  1. // This is what our customer data looks like.
  2. const customerData = [
  3. { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
  4. { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" }
  5. ];
Where customerData is an array of  "customers", each customer having several properties: ssn for the social security number, a name, an age and an email.
IndexedDB is built on a transactional database model. Everything you do in IndexedDB always happens inthe context of a transaction. The IndexedDB API provides lots of objects that represent indexes, tables, cursors, and so on, but each is tied to a particular transaction. Thus, you cannot execute commands or open cursors outside a transaction.
Example of a transaction:
  1. // Open a transaction for reading and writing on the DB "customer"
  2. var transaction = db.transaction(["customers"], "readwrite");
  3. // Do something when all the data is added to the database.
  4. transaction.oncomplete = function(event) {
  5.    alert("All done!");
  6. };
  7. transaction.onerror = function(event) {
  8.    // Don't forget to handle errors!
  9. };
  10. // Use the transaction to add data...
  11. var objectStore = transaction.objectStore("customers");
  12.  
  13. for (var i in customerData) {
  14.    var request = objectStore.add(customerData[i]);
  15.    request.onsuccess = function(event) {
  16.       // event.target.result == customerData[i].ssn
  17. };
  18. }
Transactions have a well-defined lifetime, so attempting to use a transaction after it has completed throws exceptions.
Transactions also auto-commit and cannot be committed manually.
This transaction model is really useful when you consider what might happen if a user opened two instances of your web app in two different tabs simultaneously. Without transactional operations, the two instances could stomp all over each others modifications. 
The IndexedDB API is mostly asynchronous. The API doesn't give you data by returning values; instead, you have to pass a callback function. You don't "store" a value in the database, or "retrieve" a value out of the database through synchronous means. Instead, you "request" that a database operation happens. You are notified by a DOM event when the operation finishes, and the type of event you get lets you know if the operation succeeded or failed. This sounds a little complicated at first, but there are some sanity measures baked in. After all, you are a JavaScript programmer, aren't you? ;-)
So look at previous extracts of code: transaction.oncomplete, transaction.onerror, request.onsuccess, etc...
IndexedDB uses requests all over the place. Requests are objects that receive the success or failure DOM events that were mentioned previously. They have onsuccess and onerror properties, and you can calladdEventListener() and removeEventListener() on them. They also have readyStateresult, anderrorCode properties that tell you the status of the request.
The result property is particularly magical, as it can be many different things, depending on how the request was generated (for example, an IDBCursor instance, or the key for a value that you just inserted into the database). We will see this in detail in the next page "Using IndexedDB".
IndexedDB uses DOM events to notify you when results are available. DOM events always have a typeproperty (in IndexedDB, it is most commonly set to "success" or "error"). DOM events also have a targetproperty that shows where the event is headed. In most cases, the target of an event is the IDBRequestobject that was generated as a result of doing some database operation. Success events don't bubble up and they can't be cancelled. Error events, on the other hand, do bubble, and can be cancelled. This is quite important, as error events abort whatever transactions they're running in, unless they are cancelled.
IndexedDB is object-oriented. IndexedDB is not a relational database, which has tables with collections of rows and columns. This important and fundamental difference affects the way you design and build your applications, it is an Object Store!
In a traditional relational data store, you would have a table that stores a collection of rows of data and columns of named types of data. IndexedDB, on the other hand, requires you to create an object store for a type of data and simply persist JavaScript objects to that store. Each object store can have a collection of indexes (corresponding to the properties of the JavaScript object you store in the store) that makes it efficient to query and iterate across.
IndexedDB does not use Structured Query Language (SQL). It uses queries on an index that produces a cursor, which you use to iterate across the result set. If you are not familiar with NoSQL systems, read the Wikipedia article on NoSQL.
IndexedDB adheres to a same-origin policy. An origin is the domain, application layer protocol, and port of a URL of the document where the script is being executed. Each origin has its own associated set of databases. Every database has a name that identifies it within an origin. Think of it as "an application, a Database".
What defines "same origin" does not include the port or the protocol. For example, an app in a page with this URL http://www.example.com/app/ and an app in a page with this URL http://www.example.com/dir/both can access the same IndexedDB database because they have the same origin (example.com),  http://www.example.com:8080/dir/ (different port) or https://www.example.com/dir/(different protocol), cannot be considered of the same origin (port and protocol are different fromhttp://www.example.com)


IndexedDB: definitions

This chapter can be read as is, it but it is primarily given as a reference. We recommend you skim read it, then do the next section ("using IndexedDB"), then come back to this page if you need any clarification.
These definitions come from the W3C specification. Read this page to familiarize yourself with the terms.

DATABASE

    • Each origin (you may consider as "each application") has an associated set of databases. A databasecomprises one or more object stores which hold the data stored in the database.
    • Every database has a name that identifies it within a specific origin. The name can be any string value, including the empty string, and stays constant for the lifetime of the database.
    • Each database also has a current version. When a database is first created, its version is 0, if not specified otherwise.  Each database can only have one version at any given time. A database can't exist in multiple versions at once.
    • The act of opening a database creates a connection. There may be multiple connections to a given database at any given time.

OBJECT STORE

    • An object store is the mechanism by which data are stored in the database. 
    • Every object store has a name. The name is unique within the database to which it belongs.
    • The object store persistently holds records (JavaScript objects), which are key-value pairs. One of these keys is  a kind of "primary key" in the SQL database sense. This "key" is a property that every object in the datastore must contain. Values in the object store are structured, but this structure may vary (i.e., if we store persons in a database, and use the email as "the key all objects must define", some may have first name and last name, others may have an address or no address at all, etc.)
    • Records within an object store are sorted according to the keys in ascending order.
    • Every object store also optionally has a key generator and an optional key path. If the object store has a key path, it is said to use in-line keys. Otherwise it is said to use out-of-line keys.
    • The object store can derive the key from one of three sources:
    1. A key generator. A key generator generates a monotonically increasing number every time a key is needed. This is somewhat similar to auto-incremented primary keys in a SQL database.
    2. Keys can be derived via a key path.
    3. Keys can also be explicitly specified when a value is stored in the object store.
Further details will be given in the next chapter "Using IndexedDB".

VERSION

    • When a database is first created, its version is the integer 0. Each database has one version at a time; a database can't exist in multiple versions at once.
    • The only way to change the version is by opening it with a higher version than the current one. This will start a versionchange transaction and fire an upgradeneeded event. The only place where the schema of the database can be updated is inside the handler of that event.
This definition describes the most recent specification, which is only implemented in up-to-date browsers. Old browsers implemented the now deprecated and removed IDBDatabase.setVersion() method.

TRANSACTION

From the specification: "A transaction is used to interact with the data in a database. Whenever data is read or written to the database, this is done by using a transaction.
All transactions are created through a connection, which is the transaction's connection. The transaction has a mode (readreadwrite or versionchange) that determines which types of interactions can be performed upon that transaction. The mode is set when the transaction is created and remains fixed for the life of the transaction. The transaction also has a scope that determines the object stores with which the transaction may interact."
A transaction in IndexedDB is similar to a transaction in a SQL database. It defines: "An atomic and durable set of data-access and data-modification operations". Either all operations succeed or fail. 
A database connection can have several active transactions associated with it at a time, but these write transactions cannot have overlapping scopes (they cannot work on the same data at the same time). The scope of a transaction, which is defined at creation time, determines which concurrent transactions can read or write the same data (multiple reads can occur, while writes will be sequential, only one at a time), and remains constant for the lifetime of the transaction.
So, for example, if a database connection already has a writing transaction with a scope that just covers theflyingMonkey object store, you can start a second transaction with a scope of the unicornCentaur andunicornPegasus object stores. As for reading transactions, you can have several of them, and they may even overlap. A "versionchange" transaction never runs concurrently with other transactions (reminder: we usually use such transactions when we create the object store or when we modify the schema).
Generally speaking, the above requirements mean that "readwrite" transactions which have overlapping scopes always run in the order they were created, and never run in parallel. A "versionchange" transaction is automatically created when a database version number is provided that is greater than the current database version. This transaction will be active inside the onupgradeneeded event handler, allowing the creation of new object stores and indexes.

REQUEST

The operation by which reading and writing on a database is done. Every request represents one read or one write operation. Requests are always run within a transaction. The example below adds a customer into the object store named "customers".
  1. // Use the transaction to add data...
  2. var objectStore = transaction.objectStore("customers");
  3. for (var i in customerData) {
  4.     var request = objectStore.add(customerData[i]);
  5.     request.onsuccess = function(event) {
  6.         // event.target.result == customerData[i].ssn
  7. };
  8. }

INDEX

It is sometimes useful to retrieve records in an object store through other means than their key.
An index allows the user to look up records in an object store using the properties of the values in the object stores records. Indexes are a common concept in databases. Indexes can speed up object retrieval and allow multi-criteria searches. For example, if you store persons in your object store, and add an index on the "email" property of each person, then looking for some person by email will be much faster.
An index is a specialized persistent key-value storage and has a referenced object store. For example, you have your "persons" object store that is the referenced data store, and this reference store can have an index storage associated with it that contains indexes that map email values to key values in the reference store.
The index has a list of records which holds the data stored in the index. The records in an index are automatically populated whenever records in the referenced object store are inserted, updated or deleted. There can be several indexes referencing the same object store, in which changes to the object store cause all such indexes to get updated.
An index contains a unique flag. When this flag is set to true, the index enforces that no two records in the index have the same key. If a user attempts to insert or modify a record in the index's referenced object store, such that evaluating the index's key path on the record's new value yields a result which already exists in the index, then the attempted modification to the object store fails.

KEY AND VALUES

KEY

A data value by which stored values are organized and retrieved in the object store. The object store can derive the key from one of three sources: a key generatora key path, and an explicitly specified value.
The key must be of a data type that has a number that is greater than the one before. Each record in an object store must have a key that is unique within the same store, so you cannot have multiple records with the same key in a given object store.
A key can be one of the following types: stringdatefloatand array. For arrays, the key can range from an empty value to infinity. And you can include an array within an array.
Alternatively, you can also look up records in an object store using the index.

KEY GENERATOR

A mechanism for producing new keys in an ordered sequence. If an object store does not have a key generator, then the application must provide keys for records being stored. Similar to auto-generated primary keys in SQL databases.

IN-LINE KEY

A key that is stored as part of the stored value. Example: the email of a person or a student number in an object representing a student in a student store. It is found using a key path. An in-line key can be generated using a generator. After the key has been generated, it can then be stored in the value using the key path, or it can also be used as a key.

OUT-OF-LINE KEY

A key that is stored separately from the value being stored, for instance, an auto-incremented id that is not part of the object. Example: you store persons {name:Buffa, firstName:Michel} and {name:Gandon, firstName: Fabien}, each will have a key (think of it as a primary key, an id...) that can be auto-generated or specified, but that is not part of the object stored.

KEY PATH

Defines where the browser should extract the key from a value in the object store or index. A valid key path can include one of the following: an empty string, a JavaScript identifier, or multiple JavaScript identifiers separated by periods. It cannot include spaces.

VALUE

Each record has a value, which could include anything that can be expressed in JavaScript, including: boolean,numberstringdateobjectarrayregexpundefined, and null.
When an object or an array is stored, the properties and values in that object or array can also be anything that is a valid value.
Blobs and files can be stored, (supported by all major browsers, IE > 9). The example in the next chapter stores images using blobs.

RANGE AND SCOPE

SCOPE

The set of object stores and indexes to which a transaction applies. The scopes of read-only transactions can overlap and execute at the same time. On the other hand, the scopes of writing transactions cannot overlap. You can still start several transactions with the same scope at the same time, but they just queue up and execute one after another.

CURSOR

A mechanism for iterating over multiple records with a key range. The cursor has a source that indicates which index or object store it is iterating. It has a position within the range, and moves in a direction that is increasing or decreasing in the order of record keys. For the reference documentation on cursors, see IDBCursor.

KEY RANGE

A continuous interval over some data type used for keys. Records can be retrieved from object stores and indexes using keys or a range of keys. You can limit or filter the range using lower and upper bounds. For example, you can iterate over all values of a key between x and y.
For the reference documentation on key range, see IDBKeyRange.

Introduction

This page and the following one, entitled "Using IndexedDB", will provide simple examples for creating, adding, removing, updating, and searching data in an IndexedDB database. They explain the basic steps to perform such common operations, while explaining the programming principles behind IndexedDB.
In the "Using IndexedDB" pages of this course, you will learn about:
    • Creating and populating a database
    • Working with data
    • Using transactions
    • Inserting, deleting, updating and getting data
    • Creating and populating a database

EXTERNAL RESOURCES

Additional information is available on these Web sites. Take a look at these!

Creating a database

Online example at JSBin that shows how to create and populate an object store named "CustomerDB". This example should work on both mobile and desktop versions of all major post-2012 browsers.
We suggest that you follow what is happening using the developer tools from the Google Chrome browser. Other browsers also offer equivalent means for debugging Web applications that use IndexedDB.
With Chrome, please open the JSBin example and activate the Developer tool console (F12 or cmd-alt-i). Open the JavaScript and HTML tabs on JSBin.
Then, click the "Create CustomerDB" button in the HTML user interface of the example: it will call thecreateDB() JavaScript function that:
    1. creates a new IndexedDB database and an object store in it ("customersDB"), and
    2. inserts two javascript objects (look at the console in your devtools - the Web app prints lots of traces in there, explaining what it does under the hood). Note that the social security number is the "Primary key", called a key path in the IndexedDB vocabulary (red arrow on the right of the screenshot).
Chrome DevTools (F12 or cmd-alt-i) shows the IndexedDB databases, object stores and data:
chrome dev tools show the content of the IndexedDB database that has been created and populated
Normally, when you create a database for the first time, the console should show this message:
Message displayed in console when the database is created the first time you run the example
This message comes from the JavaScript request.onupgradeneeded callback. Indeed, the first time we open the database we ask for a specific version (in this example: version 2) with:
  1. var request = indexedDB.open(dbName, 2);
...and if there is no version "2" of the database, then we enter the onupgradeneeded callback where we really create the database.
You can try to click again on the button "CreateCustomerDatabase", if database version "2" exists, this time therequest.onsuccess callback will be called. This is where we will add/remove/search data (you should see a message on the console).
Notice that the version number cannot be a float: "1.2" and "1.4" will automatically be rounded to "1".
JavaScript code from the example:
  1. var db; // the database connection we need to initialize
  2.  
  3. function createDatabase() {
  4.   if(!window.indexedDB) {
  5.      window.alert("Your browser does not support a stable version
  6.                    of IndexedDB");
  7.   }
  8.   // This is what our customer data looks like.
  9.   var customerData = [
  10.     { ssn: "444-44-4444", name: "Bill", age: 35, email:
  11.                                            "bill@company.com" },
  12.     { ssn: "555-55-5555", name: "Donna", age: 32, email:
  13.                                            "donna@home.org" }
  14.   ];
  15.   var dbName = "CustomerDB";
  16.   // version must be an integer, not 1.1, 1.2 etc...
  17.   var request = indexedDB.open(dbName, 2);
  18.   request.onerror = function(event) {
  19.      // Handle errors.
  20.      console.log("request.onerror errcode=" + event.target.error.name);
  21.   };
  22.   request.onupgradeneeded = function(event) {
  23.       console.log("request.onupgradeneeded, we are creating a
  24.                    new version of the dataBase");
  25.       db = event.target.result;
  26.       // Create an objectStore to hold information about our
  27.       // customers. We're going to use "ssn" as our key path because
  28.       //  it's guaranteed to be unique
  29.       var objectStore = db.createObjectStore("customers",
  30.                                                { keyPath: "ssn" });
  31.       // Create an index to search customers by name. We may have            
  32.       // duplicates so we can't use a unique index.
  33.       objectStore.createIndex("name", "name", { unique: false });
  34.       // Create an index to search customers by email. We want to
  35.       // ensure that no two customers have the same email, so use a
  36.       // unique index.
  37.       objectStore.createIndex("email", "email", { unique: true });
  38.       // Store values in the newly created objectStore.
  39.       for (var i in customerData) {
  40.           objectStore.add(customerData[i]);
  41.       }
  42.   }; // end of request.onupgradeneeded
  43.   request.onsuccess = function(event) {
  44.      // Handle errors.
  45.      console.log("request.onsuccess, database opened, now we can add
  46.                   / remove / look for data in it!");
  47.      // The result is the database itself
  48.      db = event.target.result;
  49.   };
  50. } // end of function createDatabase
Explanations:
All the "creation" process is done in the onupgradeneeded callback (lines 26-50):
    • Line 30: get the database created in the result of the dom event: db = event.target.result;
    • Line 35: create an object store named "customers" with the primary key being the social security number ("ssn" property of the JavaScript objects we want to store in the object store): var objectStore = db.createObjectStore("customers", {keyPath: "ssn"});
    • Lines 39 and 44: create indexes on the "name" and "email" properties of JavaScript objects:objectStore.createIndex("name", "name", {unique: false});
    • Lines 47-49: populate the database: objectStore.add(...).
Note that we did not create any transaction, as the onupgradeneeded callback on a create database request is always in a default transaction that cannot overlap with another transaction at the same time.
If we try to open a database version that exists, then the request.onsuccess callback is called. This is where we are going to work with data. The DOM event result attribute is the database itself, so it is wise to store it in a variable for later use: db = event.target.result;

Working with data

EXPLICIT USE OF A TRANSACTION IS NECESSARY

All operations in the database should occur within a transaction!
While the creation of the database occurred in a transaction that was run "under the hood" with no explicit "transaction" keyword used, for adding/removing/updating/retrieving data, explicit use of a transaction is required.
You need to get a transaction from the database object and indicate which object store the transaction will be associated with.
Source code example for creating a transaction associated with the object store named "customers":
  1. var transaction = db.transaction(["customers"], "readwrite"); // or "read"...
Transactions, when created, must have a mode set that is either readonlyreadwrite or versionchange(this last mode is only for creating a new database or for modifying its schemas: i.e. changing the primary key or the indexes).
When you can, use the transaction in readonly mode as multiple read transactions can run concurrently in this mode.
In the following pages, we will explain how to insert, search, remove and update data. A final example that merges all examples together will also be shown at the end of this section.

Inserting data in an object store

EXAMPLE 1: BASIC STEPS

Execute this example and look at the IndexedDB object store content from the Chrome dev tools (F12 or cmd-alt-i). One more customer should have been added.
Be sure to click on the "create database" button before clicking the "insert new customer" button.
example on JsBin for inserting data in IndexedDB
The next screenshot shows the IndexedDB object store in Chrome dev. tools (use the "Resources" tab). Clicking the "Create CustomerDB" database creates or opens the database, and clicking "Add a new Customer" button adds a customer named "Michel Buffa" into the object store:
Devtools show that a new customer named Michel Buffa has been inserted
Code from the example, explanations:
We just added a single function into the example from the previous section - the function AddACustomer()that adds one customer:
  1. { ssn: "123-45-6789", name: "Michel Buffa", age: 47, email:   "buffa@i3s.unice.fr" }
Here is the complete source code of the addACustomer function:
  1. function addACustomer() {
  2.    // 1 - get a transaction on the "customers" object store
  3.    // in readwrite, as we are going to insert a new object
  4.    var transaction = db.transaction(["customers"], "readwrite");
  5.    // Do something when all the data is added to the database.
  6.    // This callback is called after transaction has been completely
  7.    // executed (committed)
  8.    transaction.oncomplete = function(event) {
  9.        alert("All done!");
  10.    };
  11.    // This callback is called in case of error (rollback)
  12.    transaction.onerror = function(event) {
  13.       console.log("transaction.onerror errcode=" + event.target.error.name);
  14.    };
  15.    // 2 - Init the transaction on the objectStore
  16.    var objectStore = transaction.objectStore("customers");
  17.    // 3 - Get a request from the transaction for adding a new object
  18.    var request = objectStore.add({ ssn: "123-45-6789",
  19.                                    name: "Michel Buffa",
  20.                                    age: 47,
  21.                                    email: "buffa@i3s.unice.fr" });
  22.    // The insertion was ok
  23.    request.onsuccess = function(event) {
  24.        console.log("Customer with ssn= " + event.target.result + "
  25.                     added.");
  26.    };
  27.    // the insertion led to an error (object already in the store,
  28.    // for example)
  29.    request.onerror = function(event) {
  30.        console.log("request.onerror, could not insert customer,
  31.                     errcode = " + event.target.error.name);
  32.    };
  33. }
Explanations:
In the code above, in lines 4, 19 and 22 are the main calls you have to perform in order to add a new object to the store:
    1. Create a transaction.
    2. Map the transaction on the object store.
    3. Create an "add" request that will take part in the transaction.
The different callbacks are in lines 9 and 14 for the transaction, and in lines 28 and 35 for the request.
You may have several requests for the same transaction. Once all requests have finished, thetransaction.oncomplete callback is called. In any other case the transaction.onerror callback is called, and the datastore remains unchanged.
Here is the trace from the dev tools console:
Trace from the devtools console

EXAMPLE 2: ADDING A FORM AND VALIDATING INPUTS

a form has been added to the previous example, for creating a new customer
You can try this example by following these steps:
    1. Press first the "Create database" button
    2. then enter a new customer in the form
    3. click the "add a new Customer" button
    4. then press F12 or cmd-alt-i to use the Chrome dev tools and check for the IndexedDB store content. Sometimes it is necessary to refresh the view (right click on IndexedDB/refresh), and sometimes it is necessary to close/open the dev. tools to have a view that shows the changes (press F12 or cmd-alt-i twice). Chrome dev. tools are a bit strange from time to time.
This time we added some tests for checking that the database is open before trying to insert an element + we added a small form for entering a new customer.
Notice that the insert will fail and display an alert with an error message if:
    • The ssn is already present in the database. This property has been declared as the keyPath (a sort of primary key) in the object store schema, and it should be unique:db.createObjectStore("customers", { keyPath: "ssn" });
    • The email is already present in the object store. Remember that in our schema, the email property is an index that we declared unique: objectStore.createIndex("email", "email", { unique: true });
    • Try to insert the same customer twice, or different customers with the same ssn. An alert like this should pop up:
insert error
Here is the updated version of the HTML code of this example:
  1. <fieldset>
  2.   SSN: <input type="text" id="ssn" placeholder="444-44-4444"
  3.               required/><br>
  4.   Name: <input type="text" id="name"/><br>
  5.   Age: <input type="number" id="age" min="1" max="100"/><br>
  6.   Email:<input type="email" id="email"/> reminder, email must be
  7.                unique (we declared it as a "unique" index)<br>
  8. </fieldset>
  9. <button onclick="addACustomer();">Add a new Customer</button>
And here is the new version of the addACustomer() JavaScript function:
  1. function addACustomer() {
  2.    if(db === null) {
  3.       alert('Database must be opened, please click the Create
  4.              CustomerDB Database first');
  5.       return;
  6.    }
  7.    var transaction = db.transaction(["customers"], "readwrite");
  8.    // Do something when all the data is added to the database.
  9.    transaction.oncomplete = function(event) {
  10.      console.log("All done!");
  11.    };
  12.    transaction.onerror = function(event) {
  13.      console.log("transaction.onerror errcode=" + event.target.error.name);
  14.    };
  15.    var objectStore = transaction.objectStore("customers");
  16.    // adds the customer data
  17.    var newCustomer={};
  18.    newCustomer.ssn = document.querySelector("#ssn").value;
  19.    newCustomer.name = document.querySelector("#name").value;
  20.    newCustomer.age = document.querySelector("#age").value;
  21.    newCustomer.email = document.querySelector("#email").value;
  22.    alert('adding customer ssn=' + newCustomer.ssn);
  23.  
  24.    var request = objectStore.add(newCustomer);
  25.    request.onsuccess = function(event) {
  26.      console.log("Customer with ssn= " + event.target.result + "
  27.                   added.");
  28.      };
  29.  
  30.   request.onerror = function(event) {
  31.      alert("request.onerror, could not insert customer, errcode = "
  32.            + event.target.error.name +
  33.            ". Certainly either the ssn or the email is already
  34.            present in the Database");
  35.   };
  36. }
It is also possible to shorten the code of the above function by chaining the different operations using the "." operator (getting a transaction from the db, opening the store, adding a new customer, etc.).
Here is the short version:
  1. var request = db.transaction(["customers"], "readwrite")
  2. .objectStore("customers")
  3. .add(newCustomer);
The above code does not perform all the tests, but you may encounter such a way of coding (!).
Also note that it works if you try to insert empty data:
devtools show that inserting blank data works
Indeed, entering an empty value for the keyPath or for indexes is a valid value (in the IndexedDB sense). In order to avoid this, you should add more JavaScript code. We will let you do this as an exercise.

Removing data from an object store

devtools show that a customer has been removed once clicked on the remove customer button
See the changes in Chrome dev. tools: refresh the view (right click/refresh) or press F12 or cmd-alt-i twice. There is a bug in the refresh feature with some versions of Google Chrome.
How to try the example:
    1. Be sure to click the "create database button" to open the existing database.
    2. Then use Chrome dev tools  to check that the customer with ssn=444-44-444 exists. If it's not there, just insert into the database like we did earlier in the course.
    3. Right click on indexDB in the Chrome dev tools and refresh the display of the IndexedDB's content if necessary if you cannot see customer with ssn=444-44-444. Then click on the "Remove Customer ssn=444-44-4444(Bill)" button. Refresh the display of the database. Bill should have disappeared!
Code added in this example:
  1. function removeACustomer() {
  2.    if(db === null) {
  3.       alert('Database must be opened first, please click the
  4.              Create CustomerDB Database first');
  5.       return;
  6.    }
  7.    var transaction = db.transaction(["customers"], "readwrite");
  8.    // Do something when all the data is added to the database.
  9.    transaction.oncomplete = function(event) {
  10.       console.log("All done!");
  11.    };
  12.    transaction.onerror = function(event) {
  13.       console.log("transaction.onerror errcode=" +
  14.                    event.target.error.name);
  15.    };
  16.    var objectStore = transaction.objectStore("customers");
  17.    alert('removing customer ssn=444-44-4444');
  18.    var request = objectStore.delete("444-44-4444");
  19.    request.onsuccess = function(event) {
  20.       console.log("Customer removed.");
  21.    };
  22.    request.onerror = function(event) {
  23.       alert("request.onerror, could not remove customer, errcode
  24.             = " + event.target.error.name + ". The ssn does not
  25.             exist in the Database");
  26.    };
  27. }
Notice that after the deletion of the Customer (line 23), the request.onsuccess callback is called. And if you try to print the value of the event.target.result variable, it is "undefined".
Short way of doing the delete:
It is also possible to shorten the code of the above function a lot by concatenating the different operations (getting the store from the db, getting the request, calling delete, etc.). Here is the short version:
  1. var request = db.transaction(["customers"], "readwrite")
  2. .objectStore("customers")
  3. .delete("444-44-4444");

Modifying data from an object store

We used request.add(object) to add a new customer and request.delete(keypath) to remove a customer. Now, we will use request.put(keypath) to update a customer!
devtools show a customer being updated in IndexedDB
The above screenshot shows how we added an empty customer with ssn="", (we just clicked on the open database button, then on the "add a new customer button" with an empty form).
Then, we just filled the nameage and email input fields for updating the object with ssn="" and clicked on the "update data about an existing customer". This updates the data in the object store, as shown in this screenshot:
devtools show updated customer
Here is the code added to this example:
  1. function updateACustomer() {
  2.    if(db === null) {
  3.      alert('Database must be opened first, please click the Create
  4.             CustomerDB Database first');
  5.      return;
  6.    }
  7.    var transaction = db.transaction(["customers"], "readwrite");
  8.    // Do something when all the data is added to the database.
  9.    transaction.oncomplete = function(event) {
  10.      console.log("All done!");
  11.    };
  12.    transaction.onerror = function(event) {
  13.      console.log("transaction.onerror errcode=" + event.target.error.name);
  14.    };
  15.    var objectStore = transaction.objectStore("customers");
  16.    var customerToUpdate={};
  17.    customerToUpdate.ssn = document.querySelector("#ssn").value;
  18.    customerToUpdate.name = document.querySelector("#name").value;
  19.    customerToUpdate.age = document.querySelector("#age").value;
  20.    customerToUpdate.email = document.querySelector("#email").value;
  21.  
  22.    alert('updating customer ssn=' + customerToUpdate.ssn);
  23.    var request = objectStore.put(customerToUpdate);
  24.    request.onsuccess = function(event) {
  25.      console.log("Customer updated.");
  26.    };
  27.  
  28.    request.onerror = function(event) {
  29.      alert("request.onerror, could not update customer, errcode= " +
  30.         event.target.error.name + ". The ssn is not in the
  31.         Database");
  32.   };
  33. }

Getting data from a data store

There are several ways to retrieve data from a data store.

FIRST METHOD: GETTING DATA WHEN WE KNOW ITS KEY

The simplest function from the API is the request.get(key) function. It retrieves an object when we know its key/keypath.
Getting data from IndexedDB, first enter a ssn, then press the search button
If the ssn exists in the object store, then the results are displayed in the form itself (the code that gets the results and that updates the form is in the request.onsuccess callback).
Form updated with data retrieved
Here is the code added to that example:
  1. function searchACustomer() {
  2.    if(db === null) {
  3.      alert('Database must be opened first, please click the Create
  4.             CustomerDB Database first');
  5.      return;
  6.    }
  7.    var transaction = db.transaction(["customers"], "readwrite");
  8.    // Do something when all the data is added to the database.
  9.    transaction.oncomplete = function(event) {
  10.      console.log("All done!");
  11.    };
  12.    transaction.onerror = function(event) {
  13.      console.log("transaction.onerror errcode=" + event.target.error.name);
  14.    };
  15.    var objectStore = transaction.objectStore("customers");
  16.    // Init a customer object with just the ssn property initialized
  17.    // from the form
  18.    var customerToSearch={};
  19.    customerToSearch.ssn = document.querySelector("#ssn").value;
  20.    alert('Looking for customer ssn=' + customerToSearch.ssn);
  21.    // Look for the customer corresponding to the ssn in the object
  22.    // store
  23.    var request = objectStore.get(customerToSearch.ssn);
  24.    request.onsuccess = function(event) {
  25.      console.log("Customer found" + event.target.result.name);
  26.      document.querySelector("#name").value=event.target.result.name;
  27.      document.querySelector("#age").value = event.target.result.age;
  28.      document.querySelector("#email").value
  29.                                         =event.target.result.email;
  30.    };
  31.  
  32.    request.onerror = function(event) {
  33.      alert("request.onerror, could not find customer, errcode = " +              event.target.error.name + ".
  34.             The ssn is not in the Database");
  35.   };
  36. }
The search is done in line 30, and the callback in the case of success is request.onsuccesslines 32-38.event.target.result is the resulting object (lines 33 to 36).
Well, this is a lot of code isn't it? We can do a much shorter version of this function (though, admittedly it we won't take care of all possible errors). Here is the shortened version:
  1. function searchACustomerShort() {
  2.   db.transaction("customers").objectStore("customers")
  3. .get(document.querySelector("#ssn").value).onsuccess =   
  4.    function(event) {
  5.       document.querySelector("#name").value =
  6.                                           event.target.result.name;
  7.       document.querySelector("#age").value =
  8.                                           event.target.result.age;
  9.       document.querySelector("#email").value =
  10.                                           event.target.result.email;
  11.   }; // end of onsuccess callback
  12. }
You can try on JSBin this version of the online example that uses this shortened version (the function is at the end of the JavaScript code):
  1. function searchACustomerShort() {
  2.    if(db === null) {
  3.       alert('Database must be opened first, please click the Create
  4.              CustomerDB Database first');
  5.       return;
  6.    }
  7.    db.transaction("customers").objectStore("customers")
  8.      .get(document.querySelector("#ssn").value)
  9.      .onsuccess = 
  10.        function(event) {
  11.           document.querySelector("#name").value =
  12.                                      event.target.result.name;
  13.           document.querySelector("#age").value =
  14.                                      event.target.result.age;
  15.           document.querySelector("#email").value =
  16.                                      event.target.result.email;
  17.        };
  18. }
Explanations:
    • Since there's only one object store, you can avoid passing a list of object stores that you need in your transaction and just pass the name as a string (line 8),
    • We are only reading from the database, so we don't need a "readwrite" transaction. Calling transaction() with no mode specified gives a "readonly" transaction (line 8),
    • We don't actually save the request object to a variable. Since the DOM event has the request as its target we can use the event to get to the result property (line 9).

SECOND METHOD: GETTING MORE THAN ONE PIECE OF DATA

Getting all data in the datastore: using a cursor

Using get() requires that you know which key you want to retrieve. If you want to step through all the values in your object store, or just between a certain range, then you can use a cursor.
Here's what it looks like:
  1. function listAllCustomers() {
  2.    var objectStore =   
  3.      db.transaction("customers").objectStore("customers");
  4.    objectStore.openCursor().onsuccess = function(event) {
  5.      // we enter this callback for each object in the store
  6.      // The result is the cursor itself
  7.      var cursor = event.target.result;
  8.      if (cursor) {
  9.        alert("Name for SSN " + cursor.key + " is " +
  10.               cursor.value.name);
  11.        // Calling continue on the cursor will result in this callback
  12.        // being called again if there are other objects in the store
  13.        cursor.continue();
  14.      } else {
  15.        alert("No more entries!");
  16.      }
  17.   }; // end of onsuccess...
  18. } // end of listAllCustomers()
It adds a button to our application. Clicking on it will display a set of alerts, each showing details of an object in the object store:
Screenshot with a "list all customers button" and an alert showing one of them
The openCursor() function takes several arguments.
    • First, you can limit the range of items that are retrieved by using a key range object that we'll get to in a minute.
    • Second, you can specify the direction that you want to iterate.
In the above example, we're iterating over all objects in ascending order. The onsuccess callback for cursors is a little special. The cursor object itself is the result property of the request (above we're using the shorthand, so it's event.target.result). Then the actual key and value can be found on the key and value properties of the cursor object. If you want to keep going, then you have to call cursor.continue()on the cursor.
When you've reached the end of the data (or if there were no entries that matched your openCursor()request) you still get a success callback, but the result property is undefined.
One common pattern with cursors is to retrieve all objects in an object store and add them to an array, like this:
  1. function listAllCustomersArray() {
  2.   var objectStore =   
  3.       db.transaction("customers").objectStore("customers");
  4.   var customers = []; // the array of customers that will hold
  5.                       // results
  6.   objectStore.openCursor().onsuccess = function(event) {
  7.     var cursor = event.target.result;
  8.     if (cursor) {
  9.       customers.push(cursor.value); // add a customer in the
  10.                                     // array
  11.       cursor.continue();
  12.     } else {
  13.       alert("Got all customers: " + customers);
  14.     }
  15.  }; // end of onsucess
  16. } // end of listAllCustomersArray()

Getting data using an index

Storing customer data using the ssn as a key is logical since the ssn uniquely identifies an individual. If you need to look up a customer by name, however, you'll need to iterate over every ssn in the database until you find the right one.
Searching in this fashion would be very slow, so instead you can use an index.
Remember that we added two indexes in our data store:
    1. one on the name (non unique) and
    2. one on the email properties (unique).
Here is a function that lists by name the objects in the object store and returns the first one it finds with a name equal to "Bill":
  1. function getCustomerByName() {
  2.    if(db === null) {
  3.      alert('Database must be opened first, please click the Create
  4.             CustomerDB Database first');
  5.      return;
  6.    }
  7.    var objectStore =   
  8.       db.transaction("customers").objectStore("customers");
  9.    var index = objectStore.index("name");
  10.    index.get("Bill").onsuccess = function(event) {
  11.       alert("Bill's SSN is " + event.target.result.ssn +
  12.             " his email is " + event.target.result.email);
  13.    };
  14. }
The search by index occurs at lines 11 and 13: line 11 gets back an "index" object that corresponds to the index on the "name" property. Line 13 calls the get() method on this object to retrieve all objects that have a name equal to "Bill" in the dataStore.
retrieving data using an index. The screenshot shows a button "look for all customers named Bill", and shows an alert with the result.
The above example retrieves only the first object that has a name/index with the value="Bill". Notice that there are two "Bill"s in the object store.
Getting more than one result using an index
In order to get all the "Bills", we again have to use a cursor.
When we work with indexes, we can open two different types of cursors on indexes:
    1. A normal cursor that maps the index property to the object in the object store, or,
    2. A key cursor that maps the index property to the key used to store the object in the object store.
The differences are illustrated below.
Normal cursor:
  1. index.openCursor().onsuccess = function(event) {
  2.   var cursor = event.target.result;
  3.   if (cursor) {
  4.     // cursor.key is a name, like "Bill", and cursor.value is the
  5.     // whole object.
  6.     alert("Name: " + cursor.key + ", SSN: " + cursor.value.ssn + ",
  7.            email: " + cursor.value.email);
  8.     cursor.continue();
  9. }
  10. };
Key cursor:
  1. index.openKeyCursor().onsuccess = function(event) {
  2.    var cursor = event.target.result;
  3.    if (cursor) {
  4.      // cursor.key is a name, like "Bill", and cursor.value is the
  5.      // SSN (the key).
  6.      // No way to directly get the rest of the stored object.
  7.      alert("Name: " + cursor.key + ", "SSN: " + cursor.value);
  8.      cursor.continue();
  9.    }
  10. };
Can you see the difference? 
getting data using index. The screenshot shows two buttons: one for getting one single data and one for getting all data, using indexes
How to try this example:
    1. Press the create/Open CustomerDB database,
    2. then you may add some customers,
    3. then press the last button "look for all customers with name=Bill ...". This will iterate all the customers whose name is equal to "Bill" in the object store. There should be two "Bills", if this is not the case, add two customers with a name equal to "Bill", then press the last button again.
Source code extract from this example:
  1. function getAllCustomersByName() {
  2.   if(db === null) {
  3.     alert('Database must be opened first, please click the Create
  4.            CustomerDB Database first');
  5.     return;
  6.   }
  7.   var objectStore =
  8.      db.transaction("customers").objectStore("customers");
  9.   var index = objectStore.index("name");
  10.   // Only match "Bill"
  11.   var singleKeyRange = IDBKeyRange.only("Bill");
  12.   index.openCursor(singleKeyRange).onsuccess = function(event) {
  13.     var cursor = event.target.result;
  14.     if (cursor) {
  15.       // cursor.key is a name, like "Bill", and cursor.value is the
  16.       // whole object.
  17.       alert("Name: " + cursor.key + ", SSN: " + cursor.value.ssn ",
  18.              + email: " + cursor.value.email);
  19.       cursor.continue();
  20.    }
  21. };
  22. }

Conclusion

Hmmm... The two courses, HTML5 Part 1 and HTML5 Part 2, have covered a lot of material, and you may have trouble identifying which of the different techniques you have learnt about best suits your needs.
We have borrowed two tables from HTML5Rocks.com  that summarize the pros and cons of different approaches. 

TO SUM UP:

    • If you need to work with transactions (in the database sense: protect data against concurrent access, etc.), or do some searches on a large amount of data, if you need indexes, etc., then use IndexedDB
    • If you need a way to simply store strings, or JSON objects, then use localStorage/sessionStorage.Example: store HTML form content as you type, store a game hi-scores, preferences of an application, etc.
    • If you need to cache files for faster access or for making your application run offline, then use the cache API (as of today). In the future, look for the Service Workers API. They will be presented in future versions of this course, when browser support will be wider and the API more stabilized.
    • If you need to manipulate files (read or download/upload), then use the File API, and use XHR2.
    • If you need to manipulate a file system, there is a FileSystem and a FileWriter API which are very poorly supported and will certainly be replaced with HTML 5.1. We decided not to discuss these in the course because they don't have existing agreements from the community or the browser vendors.
    • If you need an SQL database client-side: No! Forget this idea, please! There was once a WebSQL API, but it disappeared rapidly.
Note that all the above points can be used all together: you may have a Web app that uses localStorage and IndexedDB that caches its resources so that it can run in offline mode.

WEB STORAGE

web storage pro and cons

INDEXEDDB

indexedDB pros and cons

1 comment:

  1. Companies have demonstrated that mobile applications are the best way to grow their customer base. Hence, to grow your business in this robust digital world, you need an equally robust mobile app to stay ahead of the game.
    Hire top-rated freelance mobile app developers to build interactive apps for your organization’s business goals.
    Eiliana.com freelancing portal houses top mobile app developers with a definite history of delivering high quality deliverables within the deadline.

    ReplyDelete