jsormdb——一个嵌入式 JavaScript 数据库

作者:Avi Deitcher

JavaScript 作为浏览器端开发语言的(几乎)标准化和普及,使得动态 Web 应用得以快速发展。这些应用通常看起来和感觉起来就像原生桌面应用一样流畅。再加上优秀的开源客户端开发框架,如 jQuery 和 ExtJS,以及 AJAX 的通信能力,您就拥有了一个极其强大且很大程度上开放的浏览器端开发平台。

事实上,它已经变得非常强大,以至于应用程序完全可以在浏览器中运行,而服务器则充当简单的文件存储。一个极好的例子是密码管理器,如 Clipperz 和 jkPassword。这些管理器完全在浏览器中工作,以提供用户界面和加密。服务器提供的唯一服务是提供静态 HTML 和 JavaScript 文件,以及持久化已经由浏览器端加密的文件。

随着客户端的增长,以及像这些密码管理器这样的独立应用程序的增加,对通常在开发环境中可用的基本服务的需求也在增加。在正常的开发环境中,可靠地管理数据的能力是最先要解决的关键问题之一。有时,这会导致成熟的客户端-服务器数据库系统,如 Oracle 或 MySQL。在其他情况下,这会导致嵌入式数据库——例如 Apache Derby,它们对于在应用程序的单个实例中管理数据至关重要。

不幸的是,我们的浏览器开发平台一直缺乏这样的数据管理系统。事实证明,这样一个系统的可用性在浏览器环境中甚至比在服务器环境中更为关键。对于完整的浏览器应用程序,整个数据集和事务语义都是本地的,不能依赖于服务器。对于更传统的服务器驱动应用程序,这种需求甚至更加迫切。服务器可以依赖于冗余、高带宽和低延迟来访问其数据存储,而浏览器则不具备这些属性。对于浏览器应用程序开发人员来说,能够本地执行应用程序的大部分数据活动,并以尽可能低的频率和带宽将结果发送到服务器,变得至关重要。

一种新型数据存储应运而生:嵌入式 JavaScript 数据库。本文介绍了 jsormdb,一种先进的 JavaScript 数据库引擎。jsormdb 包含了您作为开发人员在嵌入式数据库系统中期望的大部分功能,以及许多仅在浏览器环境上下文中才有意义的功能。我将介绍 jsormdb 及其基本用法,包括创建数据库、加载数据、修改和查询数据以及事务。jsormdb 还支持事件信号,以及对于浏览器端数据存储至关重要的,将更改或整个数据集持久化到服务器,但这些是另一篇文章的主题。

为什么需要数据库?

任何做过哪怕是中等程度开发的人都知道,数据存储结构在许多方面都是任何应用程序的基本构建块。这有几个很好的理由:

  1. 性能:数据结构的选择会对应用程序性能产生重大影响。例如,使用链表与串行数组相比,访问列表中间元素的速度较慢,但在列表重新排序和元素插入/添加/删除方面要快得多。

  2. 可用性:标准的数据结构使其他人(或您自己)更容易理解您的应用程序的功能及其工作原理。

  3. 关注点分离:通过将数据结构和存储与业务逻辑分离,您可以分别处理它们。

一个好的数据库结构解决了这些问题,同时提供了以下功能:

  1. 查询:您将需要查询您的数据,以发现数据的哪些元素——例如,第 1、35 和 60 条记录——符合某些任意标准。当然,大型 RDBMS 系统在这方面非常出色,甚至有自己的语言 SQL 来实现这一点。当然,您不可能将 Oracle 或 MySQL 类型的系统嵌入到 JavaScript 环境中,仅仅为了支持本地临时查询。

  2. 索引:如果您访问任意数组或数据表,并要求它返回第三个字段等于 2 的所有记录,您(或您的数据存储引擎)将需要访问每一条记录并检查第三个字段是否等于 2。这被称为全表扫描,效率非常低下。对于经常检查的字段,您会更喜欢一种更有效的方法,一种不需要线性检查每条记录的方法。这就是所谓的索引。

  3. 事务:在一个简单的世界里,事件是单阶段和单向的。例如,登录是一个简单的过程:您输入凭据,要么登录成功,要么登录失败。然而,在现实世界中,事件通常是多阶段的。例如,为了将 100 美元从您的支票账户转移到您的储蓄账户,您需要从您的支票账户中扣除 100 美元 并且 向您的储蓄账户添加 100 美元。理想情况下,这两个步骤都将成功。如果两者都失败,虽然不太好,但您可以接受。然而,如果只有其中一个成功,那么要么您(如果是扣款)要么您的银行(如果是存款)会非常不高兴。

  4. 事件:有时,您希望数据库告诉您是否发生了重要的事件。例如,您可能想知道某个表是否已更新,或者某个账户余额是否已降至某个阈值以下。许多数据库支持事件,通常称为触发器,这些触发器会引起反应。

因为我正在讨论浏览器环境,所以还有一个额外的功能很重要:持久性。浏览器环境是瞬态的;一旦用户关闭窗口,所有数据都将丢失。由于浏览器应用程序依赖于服务器来提供持久性,因此您需要一种方法来无缝地处理从服务器加载本地数据存储以及将本地更改持久化回服务器。

jsormdb 为所有这些问题提供了解决方案。当配置正确时,应用程序中的表示层和业务逻辑可以将 jsormdb 视为 整个 数据存储,而将持久化到服务器/从服务器持久化交给 jsormdb 处理。如果没有 jsormdb,您的应用程序看起来像图 1 所示。

jsormdb--an Embedded JavaScript Database

图 1. 没有数据库的 Web 应用程序

使用 jsormdb,您可以获得更简洁的设计(图 2)。

jsormdb--an Embedded JavaScript Database

图 2. 带有数据库的 Web 应用程序

工作原理

jsormdb 库引入了创建和使用数据库所需的几个概念和实现。请注意,所有这些都在 jsormdb 随附的 JavaScriptDoc 的 doc 文件夹中详细说明。

  1. 数据库:此类是 JSORM.db.db(),它代表单个数据库实例。

  2. 解析器:负责接收输入数据并将其转换为存储在 JSORM.db.db 实例中的记录,或接收 jsormdb 数据库中的条目并将其转换为可接受的输出格式。例如,您加载到库中的数据可能是 JSON、XML、JavaScript 对象甚至 Pig Latin 格式。jsormdb 并不关心,只要您提供一个解析器,可以在您首选的编码和本机 JavaScript 对象数组之间进行转换即可。jsormdb 附带了 JSON 和对象解析器;XML 解析器正在开发中。

  3. 通道:负责从远程数据存储加载数据或将数据保存到远程数据存储。HTTP 通道可以使用 AJAX 从加载 Web 页面的服务器检索数据并将数据发布到该服务器。您还可以使用另一个 jsormdb 作为数据库的源,但这不需要通道。

将所有这些组合在一起,您可以创建一个数据库实例 JSORM.db.db,它使用 JSORM.db.channel.http 通过 HTTP 从您的服务器检索数据,并使用 JSORM.db.parser.json 以 JSON 格式解析数据。图 3 显示了所有组件如何协同工作。

jsormdb--an Embedded JavaScript Database

图 3. 从服务器获取和解析数据

重要的是要注意,所有类都遵循 Douglas Crockford 的原则,并直接实例化,而无需“new”关键字。

var parser = JSORM.db.parser.json();     // right
var parser = new JSORM.db.parser.json(); // WRONG!

jsormdb 语义尽可能遵循经典 SQL 的语义。因此,添加记录是插入;修改记录是更新,等等。

安装

安装 jsormdb 涉及几个简单的步骤。

1) 从 jsorm 站点下载并解压缩库,您可以从该站点的“下载”链接获取 zip 文件。

2) 安装库。下载内容包括该库的两个版本。jsormdb.js 经过精简,大小略低于 25KB。jsormdb-src.js 未经过精简,主要用于调试,大小为 77KB。您可以使用 gzip 进一步减小它们的大小。您需要将您想要使用的库安装到您的浏览器可以访问的路径中。对于本示例,请将文件 jsormdb.js 安装在与您的 Web 页面相同的目录中。

3) 在您的 Web 页面中包含该库。通常在标头中完成,如下所示:

<script type="text/javascript" src="jsormdb.js"></script>

创建

现在您已经下载并安装了该库,并将其包含在您的页面中,您就可以创建数据库了。

在最简单的形式中,您只需实例化数据库即可创建数据库:

var db = JSORM.db.db();

虽然这会创建一个数据库,但您可能想要添加一些初始配置参数。例如,您可能想要指示要使用的解析器和/或通道,甚至直接加载一些数据。

加载数据

jsormdb 支持两种加载数据的方式:直接使用原始数据,以及通过通道远程加载。

var conf, db;
// to use a channel and parser
conf = {
    channel: JSORM.db.channel.http({updateUrl: "/send/text.php",
                                    loadUrl:   "/receive/text.json"}),
    parser:  JSORM.db.parser.json()
}
db = JSORM.db.db(conf);

// to load data directly
conf = {data: [{name: "Joe",   age: 25},
               {name: "Jill",  age: 30},
               {name: "James", age: 35}]}
db = JSORM.db.db(conf);

JSORM.db.db 有许多实例化选项。请参阅 API 文档或 Wiki 条目,这两者都在本文的“资源”中列出。

无论您选择哪种加载数据的方式,jsormdb 都期望传递给它的数据是一个简单的 JavaScript 对象字面量数组。jsormdb 对数据结构并不严格,它也不关心每个对象上存在哪些字段。以下两者都是同样有效的:

data = [{name: "Joe",   age: 25},
        {name: "Jill",  age: 30},
        {name: "James", age: 35}];

data = [{name:      "Joe",   age:  25},
        {firstName: "Jill"},
        {surname:   "James", city: "London"}];

关于记录的一个重要注意事项是,每条记录都可以有一个类型。当记录具有类型时,它会被特别标记,并且可以与其他相同类型的记录一起更容易地搜索。这可以大大提高搜索速度,类似于在 RDBMS 中将记录放入不同的表中。

data = [
	{name: "Joe",     age: 25,         type:   "person"},
	{name: "Jill",    age: 30,         type:   "person"},
	{name: "James",   age: 35,         type:   "person"},
	{name: "Fiat",    color: "yellow", type:   "car"},
	{name: "Ferrari", color: "red",    type:   "car"},
	{name: "GM",      color: "white",  type:   "car",
	                                   status: "bankrupt"}
	];
查询数据

为了使数据库有用,您需要能够检索您插入或加载的记录——也就是说,您需要查询数据库。为此,您只需调用 db.find(query)。结果将是一个 JavaScript 对象数组,其中包含与您的查询匹配的对象。如果没有记录匹配,则返回一个空数组。如果查询本身无效,则返回一个 null 对象。

查询参数本身是一个具有两个字段的对象:“where”和“fields”。where 字段告知数据库您需要匹配什么才能检索记录。它可以是简单匹配,也可以是复合匹配,将多个简单或复合匹配连接成一个更大的匹配。fields 字段可用于限制从找到的记录中返回哪些字段。

var where, results;

// simple, retrieves all records where the name field equals "John"
where  = {field: "name", compares: "equals", value: "John"};
results = db.find({where: where});

// compound, retrieves all records where name equals "John"
// or name equals "Jack"
where = {join: 'or',
         terms: [
                {field: "name", compares: "equals", value: "John"},
                {field: "name", compares: "equals", value: "Jack"}]};
results = db.find({where: where});

复合条件可以使用以下连接符连接:'and''or'。简单条件可以匹配任何字段,并且可以使用多种条件之一来比较字段,例如“equals”、“in”、“starts”、“gt”等等。API 文档和 Wiki 条目(在本文的“资源”中列出)提供了完整列表。

最后,您可以在任何级别按要检索的记录类型限制搜索。类型字段(如果可用)始终被索引,从而实现更快的搜索。

// all records of type "car" where age >= 12
where   = {field: "age", compares: "ge", value: 12, type: "car"};
results = db.find({where: where});

重要的是要注意,db.find(query) 的结果将返回记录的副本,而不是原始记录本身。因此,可以随意修改返回的结果。

修改数据

您可以通过以下几种方式之一修改数据:删除记录、添加记录或更改记录。

添加记录非常简单。只需调用 db.insert(data),其中 data 是 JavaScript 对象字面量数组:

data = [{name: "Jack",  age:  80},
        {name: "Sam",   age:  22},
        {city: "Paris", type: "location"}]
db.insert(data);

这些记录实际上将被物理插入到 jsormdb 数据库中的哪个位置是无关紧要的,就像在任何真正的数据库中一样。重要的是记录被插入,并且您可以检索它们。

要删除记录,只需调用 db.remove(query)。query 参数与 db.find() 中的完全相同。所有与 where 匹配的记录都将被立即删除。

要更改记录,只需调用 db.update(data,query)。query 参数与 db.find() 中的完全相同。data 参数是具有要更新的字段的单个 JavaScript 对象。所有字段与 where 匹配的记录都将被更新。

// for every record where the age >= 40, change the age to be 35
var update, where;
where  = {field: "age", compares: "ge", age: 40};
update = {age: 35};
db.update(update,where);
事务

如前所述,事务对于任何非平凡的事件都至关重要。jsormdb 提供了高级事务处理功能,使您可以正确管理您的更改。

事务始终启用。从您加载数据库的那一刻起,一个新的事务就启动了。您所做的所有更改——更新、删除、插入——都由 jsormdb 跟踪。当您到达事务结束时,您必须提交更改或拒绝更改。

如果您提交更改,则所有更改跟踪都将被丢弃,并启动一个新的事务。从那时起,您将无法撤消之前的任何更改。另一方面,如果您拒绝更改,则从事务开始(上次加载或上次提交或拒绝)以来的所有更改都将被撤消。此外,如果您愿意,您可以仅拒绝部分更改。例如,如果您在此事务中进行了八次更改,并且您只想撤消最后四次更改,您可以这样做。这在用户界面环境中特别有用。例如,如果您使用 jsormdb 作为数据存储编写了一个 Web 2.0 电子表格应用程序,您可能希望让用户能够逐个撤消他们的更改,以相反的顺序,可能使用 Windows 和 Linux 上的 Ctrl-Z 或 Mac 上的 Cmd-Z。在 jsormdb 出现之前,您必须手动编写对这些更改的跟踪代码。现在,您可以简单地将此功能委托给 jsormdb。每次用户单击“撤消”时,他们都会拒绝正好一次更改。

以下示例从三条记录开始,添加两条记录,修改一条记录并删除一条记录:

var data, where, db, recs;

// create and load the database
data = [{name: "Joe",   age: 25},
        {name: "Jill",  age: 30},
        {name: "James", age: 35}];
db = JSORM.db.db({data: data});

// add records
db.insert([{name: "Karl", age: 40}, {name: "Karyn"}]);

// modify Joe
db.update({data:  {age: 26},
           where:
               {field: "name", compares: "equals", value: "Joe"}});

// remove James
db.remove({where:
               {field: "name", compares: "equals", value: "James"}});

// get all of the data
recs = db.find();
// recs = [{name: "Joe",   age: 26},
//         {name: "Jill",  age: 30},
//         {name: "Karl",  age: 40},
//         {name: "Karyn"}]

// we can commit, reject or partially reject
db.commit();  // all changes are saved and a new transaction starts
// OR
db.reject();  // all changes are rolled back;
              // db.find() returns [{name: "Joe",   age: 25},
              //                    {name: "Jill",  age: 30},
              //                    {name: "James", age: 35}]
// OR
db.reject(1); // just the removal of James is rolled back

最后但并非最不重要的一点是,commit() 可以使 jsormdb 以多种格式之一更新服务器上的新数据,甚至可以根据服务器的响应更新自身。这种更改的持久性,使用 jsormdb 在浏览器端业务逻辑和表示层与服务器端存储之间进行协调,是另一篇文章的主题。

总结

总而言之,jsormdb 为 Web 浏览器应用程序开发人员提供了以下能力:将数据管理与业务和表示逻辑干净地隔离,轻松利用完整的事务语义,以及简单高效地查询、更新和修改数据,包括索引。

资源

jsorm 站点:jsorm.com

jsorm Wiki:jsorm.com/wiki

jsorm API 文档:jsorm.com/doc/api

jsormdb 示例:jsorm.com/doc/samples/jsormdb.html

Douglas Crockford 的 JavaScript 站点:www.crockford.com

MySQL:www.mysql.org

Oracle:www.oracle.com

Avi Deitcher 是一位常驻纽约的运营和技术顾问,自 Z80 和 Apple II 时代以来一直从事技术领域。他拥有哥伦比亚大学电气工程学士学位和杜克大学工商管理硕士学位,可以通过 avi@atomicinc.com 与他联系。

加载 Disqus 评论