On the HTML5 Indexed DB API - Part 3 of n

In the previous post we covered some more background on working with the Indexed DB API and briefly reviewed the mechanics of creating a new database. In this post we take a look at some important Indexed DB constructs that one must be familiar with while working with the core APIs.

Object stores

Object stores are the indexed DB equivalent of "tables" from the relational database world. All data is stored inside object stores and serves as the primary unit of storage. A database can contain multiple object stores and each store is a collection of records. Each record is a simple key/value pair. Keys must uniquely identify a particular record and can be auto-generated. The records in an object store are automatically sorted in ascending order by keys. And finally, object stores can be created and deleted only under the context of "version change" transactions. We'll see what a "version change" transaction is when we review transactions later on in this post.

Keys and Values

Each record in the object store is uniquely identified by a "key". Keys can be arrays, strings, dates or numbers. For the purpose of comparison, arrays are considered to be greater than strings which are greater than dates which in turn are considered to be greater than numbers. Keys can be "in-line" keys or not. By "in-line" we indicate to indexed DB that the key for a particular record is actually a part of the value object itself. In our notes store sample for instance, each note object has an id property which contains the unique identifier for a particular note. This is an example of an "in-line" key, i.e., the key is a part of the value object.

Whenever keys are "in-line", we must also specify a "key path", i.e. a string that signifies how the key value can be extracted from the value object. The key path for "note" objects for instance is the string "id" since the key can be extracted from note instances by accessing the "id" property. But this scheme allows for the key value to be stored at an arbitrary depth in the value object's member hierarchy. Consider the following sample value object:

var product = {
  info: {
    name: "Towel",
    type: "Indispensable hitchhiker item",
  },
  identity: {
    server: {
      value: "T01"
    },
    client: {
      value: "TC01"
    },
  },
  price: "Priceless"
};

Here, the following key path might be used:

identity.client.value

Database versioning

Indexed DB databases have a version string associated with them. This can be used by web applications to determine whether the database on a particular client has the latest structure or not. This is useful when you make changes to your database's data model and want to propagate those changes to existing clients who are on the previous version of your data model. You can simply change the version number for the new structure and check for it the next time the user runs your app and do the needful to upgrade the structure and migrate the data.

Version number changes must be performed under the context of a "version change" transaction. Before we talk about that though let's quickly review what "transactions" are.

Transactions

Like relational databases, indexed DB also performs all of its I/O operations under the context of transactions. Transactions are created through connection objects and enable atomic, durable data access and mutation. There are two key attributes for transaction objects:

  1. Scope

    The scope determines which parts of the database can be affected through the transaction. This basically helps the indexed DB implementation determine what kind of isolation level to apply to objects during the lifetime of the transaction. You can think of the scope as simply being a list of object stores that will form a part of the transaction.

  2. Mode

    The transaction mode determines what kind of I/O operation is permitted in the transaction. The mode can be:

    1. Read only

      Allows only "read" operations on the objects that are a part of the transaction's scope.

    2. Read/write

      Allows "read" and "write" operations on the objects that are a part of the transaction's scope.

    3. Version change

      The "version change" mode allows "read" and "write" operations and also allows the creation and deletion of object stores and indexes, i.e., the structure of the database can be modified only under the context of a "version change" transaction.

    Transaction objects auto-commit themselves unless they have been explicitly aborted. Transaction objects expose events to notify clients of:

    • when they complete
    • when they abort and
    • when they timeout

Creating the object store

Our notes store database will contain only a single object store to record the list of notes. As discussed earlier, object stores must be created under the context of a "version change" transaction. Let's go ahead and extend the init method of the NotesStore object to include the creation of the object store. I've highlighted the changed bits in bold.

var NotesStore = {
    name: "notes-db",
    store_name: "notes-store",
    store_key_path: "id",
    db: null,
    ver: "1.0",
    init: function (callback) {
        var self = this;
        callback = callback || function () { };
        Utils.request(window.indexedDB.open("open", this.name), function (e) {
            self.db = e.result;

            // if the version of this db is not equal to
            // self.version then change the version
            if (self.db.version !== self.version) {
                Utils.request(self.db.setVersion(self.ver), function (e2) {
                    var txn = e2.result;

                    // create object store
                    self.db.createObjectStore(self.store_name,
                                              self.store_key_path,
                                              true);
                    txn.commit();
                    callback();
                });
            } else {
                callback();
            }
        });
    },
    .

Object stores are created by calling the createObjectStore method on the database object. The first parameter is the name of the object store. This is followed by the string identifying the key path and finally a Boolean flag indicating whether the key value should be auto-generated by the database when new records are added.

Adding data to object stores

New records can be added to an object store by calling the put method on the object store object. A reference to the object store instance can be retrieved through the transaction object. Let's implement the addNote method of our NotesStore object and see how we can go about adding a new record:

    .
    addNote: function (text, tags, callback) {
        var self = this;
        callback = callback || function () { };
        var txn = self.db.transaction(null, TransactionMode.ReadWrite);
        var store = txn.objectStore(self.store_name);
        Utils.request(store.put({
            text: text,
            tags: tags
        }), function (e) {
            txn.commit();
            callback();
        });
    },

    .

This method can be broken down into the following steps:

  1. Invoke the transaction method on the database object to start off a new transaction. The first parameter is the list of names of the object stores which are going to be a part of the transaction. Passing null causes all the object stores in the database to be a part of the scope. The second parameter indicates the transaction mode. This is basically a numeric constant which we have declared like so:

    // IndexedDB transaction mode constants
    var TransactionMode = {
        ReadWrite: 0,
        ReadOnly: 1,
        VersionChange: 2
    };
    
  2. Once the transaction has been created we acquire a reference to the object store in question through the transaction object's objectStore method.

  3. Once we have the object store handy, adding a new record is just a matter of issuing an asynchronous API call to the object store's put method passing in the new object to be added to the store. Note that we do not pass a value for the id field of the new note object. Since we passed true for the auto-generate parameter while creating the object store, the indexed DB implementation should take care of automatically assigning a unique identifier for the new record.

  4. Once the asynchronous put call completes successfully, we commit the transaction.

Coming up

In the next post, we'll take a look at retrieving records from an indexed DB. Surprisingly, this turns out to be slightly trickier than what one might expect.

comments powered by Disqus