Get in touch
Thank you
We will get back to you as soon as possible

15.6.2015

7 min read

Learning IndexedDB: from CRUD to performance

Earlier I described IndexedDB in general overview and examined examples of practical IndexedDb usage. This article is continuation of previous and consists of two main parts. The first one is dedicated to usage of CRUD (Create, Read, Update, Delete) operations. The second part is about cursors that allow reading of big volumes of data and creating indexes for increasing performance while executing transaction. One of the most popular operations is adding objects to collection. For this purpose there is method “add” in ObjectStore with two parameters. The first is the object to be added, and the second (optional) – value of primary key, which uniquely identifies it in the collection. On creation of ObjectStore I passed key with name “myKey”, that is why instead of passing the second argument we can set key value directly inside of the object.

connectToDB(function(e) {
    db = e;
    var transaction = db.transaction(["name"], "readwrite");
    var nameStore = transaction.objectStore("name");
    var name = {
        myKey: 1,
        first_name: "John",
        last_name: "Smith",
        age: 31
    };
    var request = nameStore.add(name);
});

Adding is asynchronous operation, and that is why we have handlers defined for success and error.

request.onerror = function(e) {
    console.log("Error", e.target.error.name);
}
request.onsuccess = function(e) {
    console.log("Object was successfuly added");
}

After this code’s execution there will be new record added to the storage. In console, you will see following message - "Object was successfully added". If we’ll look on contents of database, you will find added previously object there. Selection_034 During creation, we have set up that unique key for the storage is field “myKey”. That is why for each record value in myKey should be unique. Let us try to call that code once again. Due to hard-coded value in our example for the field“myKey” script will try to add new record with now non-unique value. You will see following error message: "Error ConstraintError". ConstraintError means that we were trying to add record with already present key, what is exactly the case. Let us talk now about keys. They are nothing else but primary keys analogues from relational databases world. Even though IndexedDB does not store data in tables each record should have unique key, which should be used by database in order to be able to work with it. One of possible ways of organizing this is by manual setting of primary key. The second variant is key auto-generation. Thus, IndexedDB will generate unique value for primary key when it does not contain key already. To switch the auto-generation on you should pass { autoIncrement: true } as the second parameter during creation, for example:

db.createObjectStore("test2", {
    autoIncrement: true
});

Let’s check how it works. As we know all operations on database-schema change should be executed in onupgradeneeded, which can be called by updating version of database. Let’s change the version of database and create new Object Store with name «test».

function connectToDB(callback) {
    var request = window.indexedDB.open("MyDB", 4);
    request.onerror = function(err) {
        console.log(err);
    };
    request.onsuccess = function() {
        if (typeof callback !== 'undefined') {
            callback(request.result);
        }
    };
    request.onupgradeneeded = function() {
        var db = request.result;
        if (!db.objectStoreNames.contains("name")) {
            var objectStore = db.createObjectStore("name", {
                keyPath: "myKey"
            });
        }
        if (!db.objectStoreNames.contains("test")) {
            var objectStore = db.createObjectStore("test", {
                autoIncrement: true
            });
        }
    };
}

Now we try to add two objects with the same values in key-field.

connectToDB(function(e) {
    db = e;
    var transaction = db.transaction(["test"], "readwrite");
    var testStore = transaction.objectStore("test");
    var test = {
        first_name: "John",
        last_name: "Smith",
        age: 31
    };
    var request = testStore.add(test);
    var request = testStore.add(test);
    request.onerror = function(e) {
        console.log("Error", e.target.error.name);
    }
    request.onsuccess = function(e) {
        console.log("Object was successfuly added");
    }
});

Let’s have a look at the contents of database, there is object in ObjectStore with name “test” which contains two records and each of them contains unique key. Selection_036 Now when we are clear with keys let’s move to data access. Similarly to adding there is possibility of reading/removing and updating of data. To read data from storage you should create transaction, achieve access to the Object Store needed and call method get, passing key of element to be retrieved as argument for this method. Retrieved object can be found in «target.result» of resulting objects. For tests on ObjectStore “test” few new records were added. Below is the result of retrieving object with key “5”

connectToDB(function(e) {
    db = e;
    var transaction = db.transaction(["test"], "readonly");
    var testStore = transaction.objectStore("test");
    var request = testStore.get(5);
    request.onerror = function(e) {
        console.log("Error", e.target.error.name);
    }
    request.onsuccess = function(e) {
        console.log(e.target.result);
    }
});

Selection_037 To update data in storage you can use method put, also called on any ObjectStore object. There is also possible update by key. Retrieved record can be changed and after that updated in the storage. Below is the example of such action:

connectToDB(function(e) {
    db = e;
    var transaction = db.transaction(["test"], "readwrite");
    var testStore = transaction.objectStore("test");
    var request = testStore.get(2);
    request.onerror = function(e) {
        console.log("Error", e.target.error.name);
    }
    request.onsuccess = function(e) {
        var result = e.target.result;
        console.log("Previous value: ", result.age);
        result.age = 20;
        var updateRequest = testStore.put(result);
        updateRequest.onsuccess = function(e) {
            var updated = e.target.result;
            console.log("New value: ", updated.age);
        };
    }
});

Do not forget that updated object should contain key field, or it will be added to storage as a new instance. To delete object from database you should use delete on ObjectStore instance. As argument, it gets key of removable object. Here is an example:

connectToDB(function(e) {
    db = e;
    var transaction = db.transaction(["test"], "readwrite");
    var testStore = transaction.objectStore("test");
    var request = testStore.delete(6);
    request.onerror = function(e) {
        console.log("Error", e.target.error.name);
    }
    quest.onsuccess = function(e) {
        console.log('Deleted!')
    }
});

For reading of big volumes of data, there is cursor in IndexedDB. It allows us to get collection of records with possibility to iterate them. You can create cursor for any ObjectStore object, just calling method openCursor(). Below you can find example:

connectToDB(function(e) {
    db = e;
    var transaction = db.transaction(["test"], "readonly");
    var testStore = transaction.objectStore("test");
    var cursor = testStore.openCursor();
    cursor.onsuccess = function(e) {
        var res = e.target.result;
        if (res) {
            console.log("Key", res.key);
            console.log("Value", res.value);
            res.continue();
        }
    }
});

Selection_038 And finally let’s look at index creation. Indexes are one of the most important part of IndexedDB and let find data in storage based on their values and properties. Also using indexes you can set whether object’s value should be unique in the storage. First of all we should create index. Indexes are part of database schema – that is why they should be created in onupgradeneeded handler. There is example of index creation:

request.onupgradeneeded = function() {
    var db = request.result;
    if (!db.objectStoreNames.contains("name")) {
        var objectStore = db.createObjectStore("name", {
            keyPath: "myKey"
        });
        objectStore.createIndex("first_name", "first_name", {
            unique: true
        });
        objectStore.createIndex("age", "age", {
            unique: false
        });
    }
};

First parameter in method createIndexis a name of index, and the second – field being indexed. Third parameter is the object of parameters that will set configuration for index. In our case, it is set that for field “first_name” there should be unique values. When you try to add instance with the same value in this field there will be an error. How can you use it during requests? If index exists you can get it by calling index() on ObjectStore.

connectToDB(function(e) {
    db = e;
    var transaction = db.transaction(["test"], "readonly");
    var store = transaction.objectStore("test");
    var index = store.index("first_name"); //name is some value 
    var request = index.get(name);
    request.onerror = function(e) {
        console.log("Error", e.target.error.name);
    }
    request.onsuccess = function(e) {
        // do smth with e.target.result 
    }
});

Basically so we can use index for iterating through storage. To do it you should first call index() and then openCursor().

db.transaction(["test"], "readonly").objectStore.index("age").openCursor().onsuccess = function(event) {
    // do smth 
};

There was used another way to get access to Database. It is called chaining. You can chain in such a manner transaction-creation, access to ObjectStore and execution of request. You can use this when you have no need in storing the temporary objects. But also you lose possibility to handle errors occurred in-between. That’s why there should be global error handler and you can implement it subscribing on onerror event of IDBDatabase. Above I’ve described possible ways of indexes' usage to achieve values based on some particular property. But what if we want to iterate through collection only partially? in this case we'll use ranges in IndexedDB . Ranges allow us to get indexes' subset. Let’s have a look on example. Supposedly, we have collection with clients' info and we are going to collect statistics regarding persons from 23 to 30 years old. In this case, we'll use range with lower boundary set to 23 and the higher one - to 30. Ranges are being created within global object IDBKeyRange. It has three methods for setting boundary conditions on search.

  1. lowerBound is used for setting opened interval starting with lower limit and not restricted with upper one;
  2. upperBound returns range starting with upper value and containing all values below this point;
  3. bound lets to set both limits for range (lower and upper).
// returns all values over 20 
var lowerRange = IDBKeyRange.lowerBound(20); 
// returns all values over 20 inclusive 
var lowerRangeInclusive = IDBKeyRange.lowerBound(20,true); 
// returns all values under 30 
var upperRange = IDBKeyRange.upperBound(30); 
// returns all values under 30
var upperRangeInclusive = IDBKeyRange.upperBound(30, true); 
// return all values from 20 to 30 
var range = IDBKeyRange.bound(20,30) 
// return all values from 20 to 30 inclusive 
var rangeInclusive = IDBKeyRange.bound(20,30, true, true)

As soon as range is created you can pass it as argument to the openCursor(). In this case, it will return iterator that runs using index limited by range. Then task of iteration by age for ObjectStore will have following view:

connectToDB(function(e) {
    db = e;
    var transaction = db.transaction(["test"], "readonly");
    var store = transaction.objectStore("test");
    var index = store.index("age");
    var range = IDBKeyRange.bound(23, 30);
    index.openCursor(range).onsuccess = function(e) {
        var res = e.target.result;
        if (res) {
            res.continue();
        }
    }
});

IndexedDB is a nice tool for datastorage on client-side and building the autonomy web-applications. It is supported by the most part of modern browsers and has good documentation and API. All operations are executed asynchronously. The main flaw of IndexedDB is a lack of support on mobile platforms, but it can also be resolved with the usage of different wrappers. 1.http://code.tutsplus.com/tutorials/working-with-indexeddb--net-34673 2.https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB 3.http://www.w3.org/TR/IndexedDB/

0 Comments
name *
email *