内容


基于 Indexed Database 来构建 Web 应用的本地存储

Comments

Indexed Database 简介

Indexed Database(也可简称为 IndexedDB)是一个不断发展中的网络标准。这个标准用于在浏览器中存储大量结构化的数据,并提供索引以保证高效率的查询。

Mozilla 已经提交了诸多关于 IndexedDB 这个规范的技术反馈,并打算在 Firefox 4 中实现它。当 Safari,Chrome 和 Opera 等浏览器支持一种叫做 Web SQL 数据库(Web SQL Database)的技术(这种技术使用 SQL 语句作为字符串参数传递给 JavaScript API)时,从开发美学(developer aesthetic)的角度考虑,Mozilla 认为 Web SQL Database 这种应用于客户端 Web 应用程序的技术是一种很不优美的解决方案。Mozilla 带来了 Web 开发人员对于 IndexedDB 规范的反馈,并同 Microsoft 交流过,Microsoft 也赞同对于 Web 来说 IndexedDB 是一个很好的选择。

在诸多浏览器中,比如 IE 8+,Safari 4+,Chrome 4+,Opera 10.5+ 和 Firefox 2+,Web 应用程序都已经尝到了 localStorage 和 sessionStorage 的甜头:通过简单的 JavaScript API 来存储键值对。Web 存储标准(包括 localStorage 和 sessionStorage)现在已经被广泛实现,它对于存储少量的数据非常有用,但对于大量的结构化数据存储则不太适用。很多服务器端数据库都使用 SQL 来存储结构化数据,并对其进行查询,更新等各种操作,而在客户端仍使用 SQL 来操作数据则存在很大的争议。

本文中将通过比较 IndexedDB 和 Web SQL Database,你会观察到前者比后者的语法结构更为简单,并且 IndexedDB 提供给第三方 JavaScript 库更多的自由。第三方 JavaScript 库可以使用 BTree API 来实现底层的设计。IndexedDB 在浏览器层面使用底层的接口,开发者可以自己实现这些基于底层技术的接口,就像人们使用 JavaScript 实现了各种各样的框架一样,如 jQuery,Dojo 及 YUI。

Indexed Database API

同步版本的 API 只在工作线程(Worker Threads)上有用,由于并不是所有的浏览器都支持工作线程,这里讨论的 API 都是异步的 API(Asynchronous APIs)。

异步 API 方法的返回并不需要阻塞调用者线程,所有的异步操作都返回一个 IDBRequest 实例,当操作结果可用时,可以使用这个 IBDRequest 实例来访问操作结果。

1.事件接口(Event Interface)

异步操作诸如创建数据库对象,或操作数据库对象时,就会触发事件。向数据库对象发送请求时,用户代理将其相关信息载入到内存中,当这些请求对象可以被处理时,它通过触发事件来提醒应用程序。事件如下所示:

IDBEvent 接口继承自 [DOM-LEVEL-3-EVENTS]中定义的 Event。

清单 1. IDBEvent
[NoInterfaceObject]
interface IDBEvent :  {
    readonly attribute any source;
};

source 返回触发这个事件的异步请求对象

清单 2. IDBSuccessEvent
 [NoInterfaceObject] 
 interface IDBSuccessEvent : IDBEvent { 
    readonly attribute any result; 
 };

result 返回在 source 上异步请求成功完成后的结果

清单 3. IDBTransactionEvent
 [NoInterfaceObject]
interface IDBTransactionEvent : IDBSuccessEvent {
    readonly attribute IDBTransaction transaction;
};

transaction 为 IDBTransaction 类型,返回用于请求 source 的事务

清单 4. IDBErrorEvent
 [NoInterfaceObject]
interface IDBErrorEvent : IDBEvent {
    readonly attribute  unsigned short code;
    readonly attribute  DOMString message;
};

code 返回在 source 上执行异步请求时的最合适的错误代码,这个错误代码的有效值在异常 IDBDatabaseException 中定义;message 返回错误原因的描述。

清单 5. IDBVersionChangeEvent
 [NoInterfaceObject]
interface IDBVersionChangeEvent : IDBEvent {
    readonly attribute DOMString version;
};

version 返回 VERSION_CHANGE 事务发生时的数据库新版本号

2.IDBRequest 接口

IDBRequest 接口通过使用事件处理函数的属性来访问异步请求的数据库和数据库对象。例如下面的例子,我们打开一个数据库,根据不同的情形注册不同的事件处理函数:

清单 6. IDBRequest 例子
 var request = indexedDB.open('AddressBook', 'Address Book');
 request.onsuccess = function(evt) {...};
 request.onerror = function(evt) {...};
清单 7. IDBRequest
 [[NoInterfaceObject]
interface IDBRequest :  {
    void abort ();
    const unsigned short LOADING = 1;
    const unsigned short DONE = 2;
    readonly attribute unsigned short readyState;
             attribute Function onsuccess; //成功事件的处理函数
             attribute Function onerror; // 错误事件的处理函数
};

readyState 为 unsigned short 类型 ; 每个请求都以 LOADING 状态开始,当请求结束,失败或取消时,状态变为 DONE

abort:用于中断正在执行的事务请求,此方法在 readyState 为 DONE 时无效。

3.打开数据库

对象 Window 和 Worker 实现 IDBEnvironment 接口。

清单 8. IDBEnvironment
Window implements IDBEnvironment;;
Worker implements IDBEnvironment;
[NoInterfaceObject]
interface IDBEnvironment {
    readonly attribute IDBFactory indexedDB;
};

indexedDB 这个属性提供给应用程序一种访问 Indexed 数据库的机制;每一个异步请求的方法都返回一个 IDBRequest 对象,它通过事件与请求应用程序交互

清单 9. IDBFactory
 [NoInterfaceObject]
interface IDBFactory {
    readonly attribute DOMString databases;
    IDBRequest open (in DOMString name, in DOMString description) 
               raises (IDBDatabaseException);
};
 };

database 这个值是全局范围内可用的数据库名字

open 如果数据库连接打开时发生了错误,那么这个方法的返回对象将触发一个错误事件,错误事件的 code 设置为 UNKNOW_ERR,message 设置为合适的信息

4.数据库

数据库对象(database object)用来管理数据库中的对象(objects of that database)。这也是获得这个数据库事务的唯一方法。

清单 10. IDBDatabase
 [NoInterfaceObject] 
 interface IDBDatabase { 
    readonly attribute DOMString     name; 
    readonly attribute DOMString     description; 
    readonly attribute DOMString     version; 
    readonly attribute DOMStringList objectStores; 
    IDBObjectStore createObjectStore (in DOMString name, 
    [TreatNullAs=EmptyString] in optional DOMString keyPath, 
    in optional boolean autoIncrement) raises (IDBDatabaseException); 
    IDBObjectStore objectStore (in DOMString name, 
    in optional unsigned short mode, 
    in optional unsigned long timeout) raises (IDBDatabaseException); 
    void           removeObjectStore (in DOMString name) raises (IDBDatabaseException); 
    IDBRequest     setVersion ([TreatNullAs=EmptyString] in DOMString version); 
    IDBTransaction transaction (in optional DOMStringList storeNames, 
    in optional unsigned short mode, 
    in optional unsigned long timeout) raises (IDBDatabaseException); 
    void           close (); 
 };

createObjectStore 这个方法在连接的数据库中以给定的名字创建一个新的存储对象,并且返回它;返回类型:IDBObjectStore

objectStore 这个方法在连接的数据库中隐式地创建一个事务,并以给定的名字打开存储对象;返回类型:IDBObjectStore

removeObjectStore 这个方法在连接的数据库中以给定的名字销毁一个存储对象

setVersion 设置连接的数据库的版本号;返回类型:IDBRequest

transaction 这个方法返回一个 IDBTransaction 对象,并执行创建事务的操作。

5.对象存储(object store)

对象存储实现以下接口:

清单 11. IDBObjectStore
 [NoInterfaceObject]
interface IDBObjectStore {
    readonly attribute      name;
    readonly attribute      keyPath;
    readonly attribute  indexNames;
    IDBRequest put (in  value, in optional  key) raises (IDBDatabaseException);
    IDBRequest add (in  value, in optional  key) raises (IDBDatabaseException);
    IDBRequest remove (in  key) raises (IDBDatabaseException);
    IDBRequest get (in  key) raises (IDBDatabaseException);
    IDBRequest openCursor (in optional IDBKeyRange range, 
    in optional  direction) raises (IDBDatabaseException);
    IDBIndex   createIndex (in  name, in  keyPath, 
    in optional  unique) raises (IDBDatabaseException);
    IDBIndex   index (in  name) raises (IDBDatabaseException);
           removeIndex (in  indexName) raises (IDBDatabaseException);
};

add 这个方法将给定的值添加到对象存储中。如果记录能够被成功地加入到对象存储中,请求对象将触发一个成功事件。如果对象存储中已经有这个记录,这个方法的请求对象将触发一个错误事件,它的 code 设置为 CONSTRAINT_ERR;返回类型:IDBRequest

createIndex 这个方法在连接的数据库中以给定的名字和参数创建一个新的索引

get 这个方法在连接的数据库中以给定的名字取得相应的记录

index 这个方法在连接的数据库中以给定的名字打开索引

remove 这个方法在对象存储中以给定的名字删除一个记录

removeIndex 这个方法在连接的数据库中以给定的名字删除一个索引

Indexed Database 对比 Web SQL Database

为了比较 IndexedDB 和 WebDatabase,我们展示四个例子,每个例子使用每个规范的大部分异步 APIs。经过这些例子之后,带有表的 SQL 存储(WebDatabase)和带有索引的 JavaScript 对象存储(indexedDB)之间的区别将非常清晰。同步版本的 API 只在工作线程(worker thread)上有用,而目前并非所有的浏览器都实现了工作线程(worker thread),这里将不讨论同步 APIs。IndexedDB 代码是基于 Mozilla 提交给 W3C 网络应用工作组(W3C WebApp working group)的提议,这些代码已经得到了积极的反馈。简单起见,两个规范的代码都没有错误异常处理,但工程中的代码需要有错误异常处理。

这些例子是一个糖果店的销售记录,客户是小孩子。每一个 candySales 代表一笔糖果买卖。

例子 1. 打开并创建一个数据库

第一个例子将演示如何打开一个数据库连接,及版本号不正确时如果创建表格或对象存储。打开数据库操作之后,两个例子都检查版本号,并创建必要的表格或对象存储,然后设置正确的版本号。在处理版本上 WebDatabase 更加严格,如果版本号不是期望的版本号,它将给出一个错误(这通过 openDatabase 的第二个参数指定)。而 IndexedDB 只是让调用者来处理版本。

清单 12. WebDatabase 例子 1
 // 打开数据库,如果不存在则创建
 var db = window.openDatabase("CandyDB", "", 
                             "My candy store database", 
                             1024); 
 // 处理版本号
 if (db.version != "1") { 
  db.changeVersion(db.version, "1", function(tx) { 
    // User's first visit.  Initialize database. 
    var tables = [ 
      { name: "kids", columns: ["id INTEGER PRIMARY KEY", 
                                "name TEXT"]}, 
      { name: "candy", columns: ["id INTEGER PRIMARY KEY", 
                                 "name TEXT"]}, 
      { name: "candySales", columns: ["kidId INTEGER", 
                                      "candyId INTEGER", 
                                      "date TEXT"]} 
    ]; 
 
    for (var index = 0; index < tables.length; index++) { 
      var table = tables[index]; 
 // 创建表格
      tx.executeSql("CREATE TABLE " + table.name + "(" + 
                    table.columns.join(", ") + ");"); 
    } 
  }, null, function() { loadData(db); }); 
 } 
 else { 
  // User has been here before, no initialization required. 
  loadData(db); 
 }
清单 13. IndexedDB 例子 1
 // 打开数据库,如果不存在则创建
 var request = window.indexedDB.open("CandyDB", 
                                    "My candy store database"); 
 request.onsuccess = function(event) { 
  var db = event.result; 
 // 修改版本
  if (db.version != "1") { 
    // User's first visit, initialize database. 
    var createdObjectStoreCount = 0; 
    var objectStores = [ 
      { name: "kids", keyPath: "id", autoIncrement: true }, 
      { name: "candy", keyPath: "id", autoIncrement: true }, 
      { name: "candySales", keyPath: "", autoIncrement: true } 
    ]; 
 
    function objectStoreCreated(event) { 
      if (++createdObjectStoreCount == objectStores.length) { 
        db.setVersion("1").onsuccess = function(event) { 
          loadData(db); 
        }; 
      } 
    } 
 
    for (var index = 0; index < objectStores.length; index++) { 
      var params = objectStores[index]; 
 // 创建对象存储(object store)
      request = db.createObjectStore(params.name, params.keyPath, 
                                     params.autoIncrement); 
      request.onsuccess = objectStoreCreated; 
    } 
  } 
  else { 
    // User has been here before, no initialization required. 
    loadData(db); 
  } 
 };

例子 2. 存储孩子到数据库中

这个例子存储几个孩子到表格或存储对象中。这个例子将演示使用 WebDatabase 时必须处理的一个风险:SQL 注入攻击(SQL Injection Attack)。在 WebDatabase 中必须使用显式的事务,但在 IndexedDB 中如果只有一个对象存储被访问,则会自动提供事务。IndexedDB 可以插入 JavaScript 对象,而在 WebDatabase 中调用者必须自己组合 JavaScript 对象的这些列。

清单 14. WebDatabase 例子 2
 var kids = [ 
  { name: "Anna" }, 
  { name: "Betty" }, 
  { name: "Christine" } 
 ]; 
 // 打开数据库
 var db = window.openDatabase("CandyDB", "1", 
                             "My candy store database", 
                             1024); 
 db.transaction(function(tx) { 
  for (var index = 0; index < kids.length; index++) { 
    var kid = kids[index]; 
   // 执行插入操作
    tx.executeSql("INSERT INTO kids (name) VALUES (:name);", [kid], 
                  function(tx, results) { 
      document.getElementById("display").textContent = 
          "Saved record for " + kid.name + 
          " with id " + results.insertId; 
    }); 
  } 
 });
清单 15. IndexedDB 例子 2
 var kids = [ 
  { name: "Anna" }, 
  { name: "Betty" }, 
  { name: "Christine" } 
 ]; 
 // 打开数据库 
 var request = window.indexedDB.open("CandyDB", 
                                    "My candy store database"); 
 request.onsuccess = function(event) { 
  var objectStore = event.result.objectStore("kids"); 
  for (var index = 0; index < kids.length; index++) { 
    var kid = kids[index]; 
   // 插入数据
    objectStore.add(kid).onsuccess = function(event) { 
      document.getElementById("display").textContent = 
        "Saved record for " + kid.name + " with id " + event.result; 
    }; 
  } 
 };

例子 3. 列举所有的孩子

这个例子列举所有存储于 kids 表或 kids 对象存储中的孩子。WebDatabase 使用结果集对象(Result Set Object),而 IndexedDB 在结果收集后传递一个 cursor 给事件处理函数。

清单 16. WebDatabase 例子 3
 // 打开数据库
 var db = window.openDatabase("CandyDB", "1", 
                             "My candy store database", 
                             1024); 
 db.readTransaction(function(tx) { 
  // 遍历整个表,function(tx,results) 用以列举所有的孩子
  tx.executeSql("SELECT * FROM kids", function(tx, results) { 
    var rows = results.rows; 
    for (var index = 0; index < rows.length; index++) { 
      var item = rows.item(index); 
      var element = document.createElement("div"); 
      element.textContent = item.name; 
      document.getElementById("kidList").appendChild(element); 
    } 
  }); 
 });
清单 17. IndexedDB 例子 3
 var request = window.indexedDB.open("CandyDB", 
                                    "My candy store database"); 
 request.onsuccess = function(event) { 
  // 遍历整个对象存储
  request = event.result.objectStore("kids").openCursor(); 
  request.onsuccess = function(event) { 
    var cursor = event.result; 
    // 如果 cursor 不为空, 则我们已经完成了遍历。
    if (!cursor) { 
      return; 
    } 
    var element = document.createElement("div"); 
   // 通过 cursor 访问查询结果
    element.textContent = cursor.value.name; 
    document.getElementById("kidList").appendChild(element); 
    cursor.continue(); 
  }; 
 };

例子 4. 列举所有买糖果的孩子

这个例子列举所有的孩子,并且列举每个孩子购买的糖果数。WebDatabase 使用左连接查询(left join query)使得本例非常简单。而 IndexedDB 目前并没有针对对象存储之间的连接查询的 API,这里打开两个 cursor,一个是 kids 对象存储的,另一个是 candySales 对象存储上的索引 kidId 的,然后做手工的连接。

清单 18. WebDatabase 例子 4
 var db = window.openDatabase("CandyDB", "1", 
                             "My candy store database", 
                             1024); 
 db.readTransaction(function(tx) { 
      // 这里执行左连接操作,连接 kids 和 candySales.kidId 
  tx.executeSql("SELECT name, COUNT(candySales.kidId) " + 
                "FROM kids " + 
                "LEFT JOIN candySales " + 
                "ON kids.id = candySales.kidId " + 
                "GROUP BY kids.id;", 
                function(tx, results) { 
    var display = document.getElementById("purchaseList"); 
    var rows = results.rows; 
    for (var index = 0; index < rows.length; index++) { 
      var item = rows.item(index); 
      display.textContent += ", " + item.name + "bought " + 
                             item.count + "pieces"; 
    } 
  }); 
 });
清单 19. IndexedDB 例子 4
 candyEaters = []; 
 function displayCandyEaters(event) { 
  var display = document.getElementById("purchaseList"); 
  for (var i in candyEaters) { 
    display.textContent += ", " + candyEaters[i].name + "bought " + 
                           candyEaters[i].count + "pieces"; 
  } 
 }; 
 
 var request = window.indexedDB.open("CandyDB", 
                                    "My candy store database"); 
 request.onsuccess = function(event) { 
  var db = event.result; 
  var transaction = db.transaction(["kids", "candySales"]); 
  transaction.oncomplete = displayCandyEaters; 
 
  var kidCursor; 
  var saleCursor; 
  var salesLoaded = false; 
  var count; 
 
 // 得到 kids 的结果集
  var kidsStore = transaction.objectStore("kids"); 
  kidsStore.openCursor().onsuccess = function(event) { 
    kidCursor = event.result; 
    count = 0; 
    attemptWalk(); 
  } 
 // 再得到 candySales 的结果集
  var salesStore = transaction.objectStore("candySales"); 
  var kidIndex = salesStore.index("kidId"); 
  kidIndex.openObjectCursor().onsuccess = function(event) { 
    saleCursor = event.result; 
    salesLoaded = true; 
    attemptWalk(); 
  } 
  function attemptWalk() { 
    if (!kidCursor || !salesLoaded) 
      return; 
    // 这里手动做 kids 和 candySales 的结果集左结合
    if (saleCursor && kidCursor.value.id == saleCursor.kidId) { 
      count++; 
      saleCursor.continue(); 
    } 
    else { 
      candyEaters.push({ name: kidCursor.value.name, count: count }); 
      kidCursor.continue(); 
    } 
  } 
 }

IndexedDB 整体上简化了数据库交互的编程模型,并且适用范围更广。它可以被第三方 JavaScript 库封装,例如,可以将 CouchDB 类型的 API 构建在 IndexedDB 的实现上,当然也很有可能将 SQL 类型的 API 构建在 IndexedDB 之上(例如 WebDatabase)。

结束语

本文 介绍了 Indexed Database,并通过几个例子比较了 Web SQL Database 和 Indexed Database,相比之下 Indexed Database 的编程模型更加简单。由于 Indexed Database 的规范编辑并没有结束,让我们拭目以待。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=748968
ArticleTitle=基于 Indexed Database 来构建 Web 应用的本地存储
publish-date=07282011