与传统模式相比,面向服务的互联网应用提供一种更加快速,方便的方式来发布、处理信息。其分布式及松耦合的特性也受到越来越多企业的青睐。随着企业的发展,应用程序的数据量往往呈几何级规模快速膨胀,客户端和服务器端之间交换的数据格式也是多种多样,常见的比如有 JSON,XML 等。伴随 Web2.0、RIA 的发展,在客户端处理数据逐渐成为一种趋势,但是基于 XMLHTTPRequest 的一般 Ajax 客户端程序必须由 Web 开发人员自己编写处理各种数据格式的代码。这样,不仅加重了客户端逻辑的复杂性,而且降低了程序的可维护性和可扩展性。而 Dojo Data 库旨在为不同的数据格式提供一种统一的数据访问模型,使得数据的读写都采用统一的接口,从而有利于程序的移植和维护。
本文将介绍 Dojo Data 库的工作原理,及常用的 API,并结合一个具体的应用场景,介绍如何使用 Dojo Data 库提供的 dojo.data.ItemFileReadStore 和 dojox.data.XmlStore 存储库来实现 Web 客户端数据的获取、分页、排序、过滤查找等功能,最后简要介绍了 Dojo Data 库的 Write API。
在富客户端应用程序中,客户端和服务器端交换的数据格式多种多样(比如 JSON,XML 等),因此,一个基于客户端的统一数据访问层成为富客户端应用程序的基础。Dojo 提供了许多存储库来访问不同格式的数据,如 dojo.data.ItemFileReadStore(用来读取 JSON 文件),dojox.data.XmlStore,dojox.data.AtomReadStore,dojox.data.CsvStore,和 dojox.data.OpmlStore 等。Dojo Data 的目标是为不同的客户端应用或 widget 等提供一个统一的数据访问接口,以达到互操作性的目的,而各种客户端应用不需要了解各种诸如 JSON、XML、CSV 等数据的具体格式,通过统一的访问接口就能轻松的访问和操作数据。简单的说,我们可以把 Dojo.Data 理解为位于 dojo.xhrGet() 之上的数据解析层。与之不同的是,Dojo.xhrGet() 只负责异步的读取数据,而 Dojo Data 还要将具体的数据解析成通用的数据访问模型(数据项及其属性)。如下图所示:
图 1.Dojo Data 结构图
![](https://i-blog.csdnimg.cn/blog_migrate/17dec00b4ee99a4912fcc1b873e424ee.png)
与 JDBC 或者是 ODBC(它们屏蔽了底层数据库的差异)不同,Dojo Data 是基于客户端的数据访问层。尽管如此,我们可以列举出很多 Dojo Data 和关系数据库之间概念上的相似之处。如下表所示:
表 1.Dojo Data 与关系数据库概念比较
Dojo Data 概念 | 对应的关系数据库概念 | 描述 |
---|---|---|
DataStore | 游标 | 从数据源读取数据的 javaScript 对象,使得我们利用 Dojo Data API 存取数据时就像存取数据库中的每一条记录一样。 |
Data source | 数据库 | 数据源。一般说来,数据源可以是文件,数据库,web 服务等。 |
item | 行 | 一条具有属性名值对的数据项 |
attribute | 列 | 一个数据项的属性 |
value | -- | 一个数据项的属性的值 |
reference | -- | 在一个数据项中指向另外一个数据项的值 |
identity | 主键 | 数据项的一个或多个属性,用于唯一标识该数据项 |
query | SQL Select 语句的 where 查询子句 | 向数据源请求一个符合条件的数据集。该查询条件建议用名值对的形式表示。 |
Dojo Data APIs | JDBC 或者是 ODBC | Datastore 的标准实现,它包含一系列的 API,例如读和写,一个 datastore 可以实现其中一个或者是多个 API |
internal data representation | --- | 一个私有的数据结构,datastore 用来在本地内存中缓存数据(例如 XML DOM 结点,匿名的 JSON 对象,或者是一些数据。) |
request | SQL Select 查询 | 一系列用来修饰或者是排序数据的参数,包括查询,排序,大小写限制,或者是回调。 |
Dojo Data API 是一套设计良好的 API,它遵循着一定的设计原则:
- 数据访问被分解成独立的 API,包括 read,write,identify,notifaction 等,因而不同的存储库可以选择实现不同的 API,如下表所示:
表 2.Dojo Data API 列表
Dojo Data API | 描述 |
---|---|
Dojo.data.api.read | 提供读取数据项或者其属性值的功能,同时也用来搜索,排序,和过滤数据。 |
Dojo.data.api.write | 提供创建,删除,更新数据项或者其属性值的功能。然而并不是所有的应用和服务都提供更新数据的功能,比如说 Flickr,Google Map 等都是只提供只读功能的应用。 |
Dojo.data.api.identify | 提供基于唯一的标示符来定位和查询数据项的功能,然而并不是所有的数据格式都提供唯一的标示符。 |
Dojo.data.notification | 提供当 datastore 的数据项改变等事件发生时通知侦听器的功能。最基本的事件包括数据的创建,修改和删除等。这对于那些需要不断的轮询服务器来更新数据的应用来说,显得非常重要。 |
在本文中将重点介绍 read API。
- 一般来说,数据项和其属性的访问、修改、创建和删除都用 Dojo Data API 来实现,并不直接访问数据项对象里的属性。
有了对 Dojo Data 的基本了解之后,本文将介绍如何利用 Dojo Data API 建立统一的数据访问模型。具体可以分为以下几个方面:
- 建立针对于具体数据格式的存储库。例如,Dojo 提供了 ItemFileReadStore、XMLStore、CSVStore 等来分别处理 JSON、XML、CSV 等格式的数据。
- 利用 Dojo Data Read API 完成对数据模型的读操作,常用的 API 包括:
表 3 常用 Read API
API | 描述 |
---|---|
fetch | 定义获取数据后的处理逻辑,可以通过指定参数来实现数据的分页、排序、查找过滤等 |
getAttributes | 获得某数据项的所有属性名 |
getValues | 获得某数据项的所有属性值 |
getValue | 获得某数据项的某个属性值 |
hasAttribute | 判断该数据项是否包含某个属性 |
- 利用 Dojo Data Write API 完成对数据模型的写操作,常用的 API 包括:
表 4 常用 Write API
API | 描述 |
---|---|
newItem | 创建新的数据项 |
setValue | 更新数据项信息 |
deleteItem | 删除数据项 |
下面,本文将结合一个具体的应用场景,介绍如何使用 Dojo Data 中已有的 dojo.data.ItemFileReadStore 存储库和 dojox.data.XmlStore 存储库来提供统一的数据访问模型,实现数据的获取,分页,排序,以及过滤查找。
在我们的应用场景中,某公司有两个分公司:分公司 A 和分公司 B。由于历史原因,分公司 A、B 都保留着各自独立的员工管理系统,而他们又以 Web 服务的方式分别以 JSON 和 XML 的格式来提供数据。现在,我们需要提供一个统一的访问入口来分别展现两个分公司的员工信息。由于这两个分公司系统以不同的格式(JSON 和 XML)来提供数据,因此我们可以利用 Dojo Data 所提供的统一数据访问 API,而不用关心后端分公司的内部数据格式。
图 2. 应用场景图示
![](https://i-blog.csdnimg.cn/blog_migrate/be910d4e11114a02046072c5cc214ab4.png)
一个员工记录所包含的属性如表 3 所示:
表 5. 员工记录属性
员工号 | 姓名 | 性别 | 出生日期 | 所属部门 | 入职时间 | 上级主管人员 |
---|
分公司 A 以 JSON 的格式提供员工数据信息,以下是从分公司 A 提供的 Web 服务中获取的数据信息片段:
清单 1 JSON 格式的员工数据信息
{ identifier: 'sn', items: [ { sn: '092334', name: ' 刘君 ', sex: ' 男 ' ,dateOfBirth: '1966-9-29', dept: ' 研发 ', onboard: '1999-2-20', reportMgr: '082322'}, { sn: '099871', name: ' 李丽 ', sex: ' 女 ' ,dateOfBirth: '1979-8-19', dept: ' 销售 ', onboard: '2000-1-22', reportMgr: '072121'}, { sn: '099890', name: ' 周晶 ', sex: ' 女 ' ,dateOfBirth: '1976-8-8', dept: ' 研发 ', onboard: '2003-5-10', reportMgr: '092334'}, { sn: '105632', name: ' 张涛 ', sex: ' 男 ' ,dateOfBirth: '1984-9-29', dept: ' 研发 ', onboard: '2007-2-20', reportMgr: '099890'}, … ] } |
分公司 B 以 XML 的格式提供员工数据信息,以下是从分公司 B 提供的 Web 服务中获取的数据信息片段:
清单 2 XML 格式的员工数据信息
<?xml version="1.0" encoding="utf-8"?> <employees> <employee> <sn>192334</sn> <name> 李四 </name> <sex> 男 </sex> <dateOfBirth>1966-6-29</dateOfBirth> <dept> 研发 </dept> <onboard>1997-1-20</onboard> <reportMgr>182322</reportMgr> </employee> <employee> <sn>19636</sn> <name> 王梅 </name> <sex> 女 </sex> <dateOfBirth>1980-6-21</dateOfBirth> <dept> 人力资源 </dept> <onboard>2003-12-22</onboard> <reportMgr>191323</reportMgr> </employee> ... </employees> |
Dojo Data 提供了现成的存储库 dojo.data.ItemFileReadStore 用来读取和解析 JSON 格式的数据。我们通过如下代码新建一个存储库,用来处理分公司 A 的员工管理系统所提供的数据。
清单 3 建立存储库 dojo.data.ItemFileReadStore
var employeeStore = new dojo.data.ItemFileReadStore({ url: "http://host/ajaxproxy?serviceurl=http:// filialeA/hr/employee" }); |
其中,url 属性指定了需要访问的数据资源的地址。因为浏览器的跨域限制,我们需要通过服务器端的代理来访问其他服务器域的数据资源。http://host/ajaxproxy 是服务器端代理的地址,通过参数 serviceurl 设置真实的外部服务器域的地址。
Dojo Data 同时也提供了存储库 dojox.data.XmlStore 来处理 XML 格式的数据。我们可以通过如下代码新建一个存储库,来处理分公司 B 的员工管理系统提供的数据。
清单 4 建立存储库 dojox.data.XmlStore
var employeeStore = new dojox.data.XmlStore ({ url: " http://host/ajaxproxy?http:// filialeB/hr/employee" }); |
由于有了不同数据格式的存储库,而在不同存储库上的处理操作又都是按照 Dojo Data 定义的标准 Read API 来进行的。因此,接下来我们介绍的处理过程对于分公司 A 和分公司 B 都是一样的。
数据的获取,对于一个信息系统来说,是一个最基本也是最重要的一个功能。Dojo Data Read API 为异步获取异构数据提供了很大的便利性和灵活性。通过 Dojo.Data 我们可以获取数据源的所有数据。在本节中,我们将通过上述的应用场景来展示如何利用 Dojo Data 来读取数据。
- API
简单来讲,异步的获取数据是通过 fetch 方法来实现的。其中的异步操作是通过 onComplete 属性所指定的回调方法来实现的,它定义了数据加载完毕客户端的处理逻辑。
- fetch: function(/* Object */ keywordArgs)
异步获取一个数据项集合。参数 keywordArgs 可以是 dojo.data.api.Request 的一个实例,也可以是包含一些特定参数的 JavaScript 对象,这些参数可以实现特定的行为,比如分页,排序,查找等。
其中,onComplete 属性指定了所有数据项获取完毕后对应的回调函数。
- getAttributes: function(/* item */ item)
返回该数据项的所有属性名。参数 item 指定了当前正在访问的数据项。
- getValue: function(/* item */ item, /* attribute-name-string */ attribute, /* value? */ defaultValue)
返回该数据项当前属性的属性值。参数 item 指定了当前正在访问的数据项。attribute 指定了当前访问的属性,而 defaultValue(可选)指定了当前属性的默认值,若当前属性没有值,则将返回该默认值。
- 示例
在这个示例中,我们将获取某分公司的所有员工的基本信息,并以表格的形式展现出来。
清单 5 获取员工的基本信息
// 数据加载完毕,渲染表格或者更新 var itemsLoaded = function(items, request){ var myTable = dojo.byId("myTable"); if (myTable != null) { if (myTable.firstChild) myTable.removeChild(myTable.firstChild); var tableBody = dojo.doc.createElement("tbody"); myTable.appendChild(tableBody); // 创建表头 var tableHeader = dojo.doc.createElement("tr"); tableBody.appendChild(tableHeader); if (items && items[0]) { var attributes = employeeStore.getAttributes(items[0]); dojo.forEach(attributes, function(attr){ var th = dojo.doc.createElement("th"); tableHeader.appendChild(th); th.appendChild(dojo.doc.createTextNode(attr)); }); } // 渲染表体 var row = null; dojo.forEach(items, function(item){ // 渲染每一行 row = dojo.doc.createElement("tr"); tableBody.appendChild(row); var attributes = employeeStore.getAttributes(item); dojo.forEach(attributes, function(attribute){ var td = dojo.doc.createElement("td"); row.appendChild(td); td.appendChild(dojo.doc.createTextNode( employeeStore.getValue(item, attribute))) ; }); }); } }; // 定义 request 对象 var request=null; // 通过 fetch 方法获取数据 request = employeeStore.fetch({ onComplete: itemsLoaded }); |
以上代码片段通过 fetch 方法获取所有员工的基本信息,然后回调 onComplete 对应的方法,用来渲染表格及加载数据。在加载数据时用到了 Dojo Data Read API 中的 getAttributes 方法和 getValue 方法,前者获得当前数据项的所有属性名,后者返回当前数据项指定属性的属性值。
- 提高用户体验度
作为补充,这里我们再介绍另外一种替代方案,即不用 onComplete 指定的回调方法,改用 onItem 指定回调方法,两者的区别在于,onComplete 对应数据全部加载完毕之后客户端的处理逻辑,而 onItem 对应每一个数据项加载完毕之后客户端的处理逻辑。显然,后者更加体现了异步的优势,尤其是在网络条件不好的情况下,客户端不用等待所有数据加载完毕一起显示,而是读到一条显示一条,最大程度的提高了用户体验度。当然,我们也可以两者结合起来使用,此时,onComplete 对应的回调方法中的 items 参数会为空,但是可以在此方法中完成一些数据加载完毕后的额外数据处理逻辑。
前面介绍了如何通过 Read API 的 fetch 方法查询并获得一个数据集合。但通常的情况是,因为应用程序界面设计的考虑,不希望一次展现所有返回的数据集合。这时,我们往往需要通过分页来提供更好的用户体验。分页通常可在服务器端实现,通过服务器端脚本来控制分页逻辑。而 Dojo Data 同样提供了分页机制,不同的是其分页的处理是在客户端。通过设置 Dojo Data Read API 中的 fetch 方法的相关分页选项便能方便的实现分页。
- API
简单来讲,分页机制是通过在 fetch 方法中指定一个 start 参数和一个 count 参数来起作用的。Start 参数决定了 fetch 方法从那里开始返回数据项。数据集里的第一个数据索引为 0。第二个参数 count 指定了从 start 参数指定的位置开始,要返回的数据项的数量。默认情况下,如果 start 和 count 不指定,从第一个数据开始,一直到数据集合结束,返回所有的数据项。
在如下的示例中,我们将用到如下 Read API:
fetch: function(/* Object */ keywordArgs)
异步地获取一个数据项集合。
通过在 keywordArgs 中指定 start 和 count 属性来控制分页。
- 示例
在我们的示例中要求每页显示 10 个员工信息,同时页面上会显示所有的跳转到分页面的链接,供用户在各个页面中获取数据。
清单 6 分页获取员工信息
// 分页 var pageSize = 10; var generatePaginator = function(size, request){ // 计算需要把记录分成多少个页面 var totalPage = Math.ceil(size / pageSize); // 动态生成跳转到所有分页的链接 var paginatorDomNode = dojo.byId("paginator"); if (paginatorDomNode) { // 清理之前已有的分页链接 while (paginatorDomNode.hasChildNodes()) { paginatorDomNode.removeChild(paginatorDomNode.firstChild); for (i = 1; i <= totalPage; i++) { var currentPage = request.start/pageSize + 1; if (i != currentPage) { var pageLink = dojo.doc.createElement("a"); pageLink.href = "javascript:;"; pageLink.appendChild(dojo.doc.createTextNode(i)); dojo.connect(pageLink, "onclick", function(e){ request.start = e.target.firstChild.nodeType == 3 ? (e.target.firstChild.nodeValue - 1) * pageSize : 0; employeeStore.fetch(request); }); paginatorDomNode.appendChild(pageLink); }else{ paginatorDomNode.appendChild(dojo.doc.createTextNode(currentPage)); } paginatorDomNode.appendChild(dojo.doc.createTextNode("|")); } } }; // 通过 fetch 方法获取数据,并设置分页属性 request = employeeStore.fetch({ onBegin: generatePaginator, onComplete: itemsLoaded, start: 0, count: pageSize }); |
在我们的示例代码中用到了 onBegin 回调函数,它带有 size 和 request 两个参数,其中 size 返回的是当前存储库中所有员工记录的数量。在得知员工记录总数量和每页需要显示的员工数量后,我们便可以计算出分页的数量。最终带有分页功能的员工记录展现界面如下:
图 3. 带分页功能的界面
![](https://i-blog.csdnimg.cn/blog_migrate/15878041a9f4418ec0269dd8a988091a.png)
- 性能考虑
因为 Dojo Data 的分页是一次性把 server 端返回的数据格式作为一个存储库,然后再在客户端进行具体的分页操作,所以它并不适合处理超大量的数据。因为所有的数据都必须下载到客户端,这样会降低客户端的性能。如果是超大量的数据,并希望获得分页功能,最好的办法还是在服务器端来实现分页逻辑。
上节中获得的数据集合是无序的,然而,在我们的人事管理系统中,排序是一个不可或缺的功能,例如我们需要按照员工的名字进行排序,然后展示给终端用户。
- API
一般说来,数据的排序是 fetch 方法中指定 sort 参数来完成的。Sort 参数不仅指定了要排序的字段,而且还必须指定排序的顺序即升序还是降序。
fetch: function(/* Object */ keywordArgs)
异步地获取一个数据项集合。
通过在 keywordArgs 中指定 sort 对象来控制排序。其中 sort 是一个包含 attribute 属性和 descending 属性的 JavaScript 对象数组。对于每一个对象,attribute 属性用来指定要被排序的数据项属性,descending 属性指定按升序还是降序来排序。true 为降序,false 为升序。当指定了多个排序的对象时,排在前面的拥有较高的优先级。
- 示例
在本节的示例中,我们将以上节获得的数据为基础,并在此基础上,实现能按照表格中的名字进行排序的功能。
清单 7 对数据进行排序
// 排序 var sortName= function(){ request = employeeStore.fetch({ onComplete: itemsLoaded, start: startIndex, count: pageSize, sort: [{ attribute: "name", descending: false }], start: 0, count: pageSize }); }; // 通过 dojo.connect 关联排序操作 dojo.connect(dojo.byId("sortByName"), "onclick", sortName); |
由上述代码可知,当我们点击数据项中的名字列头时,当前页面会按照名字进行升序排序。当然,这里的代码只是起一个示范作用,更复杂更全面的代码,可参考 dojo.grid 的源码实现。注意,如果某一个返回的数据项在指定排序的 attribute 列上没有值 (undefined),同时是按升序排序时,则该数据项将出现在所有被排序数据项的最底部。
除了展现公司中所有的员工信息,有时候我们还需要查找一部分特定的员工信息。比如,所有研发部门的员工信息,或所有 2005 年入职的员工信息。为了满足这个需求,我们可以在客户端取出所有的员工数据,之后在一个循环中进行筛选过滤,最终得到我们所需要的数据。这个方法虽然可行,但显得繁琐。幸运的是,Dojo Data 提供了查找过滤的机制,利用这套机制,我们可以用最少的代码来完成对所需数据的过滤。
- 设置查询条件(query)
若要实现对数据的查询过滤,我们首先要设置相应的查询条件。Dojo Data 推荐使用 Javascript 对象来表述具体的查询条件,其功能类似 SQL Select 语句中的 where 查询子句,而且也同样分为精确匹配查询和模糊匹配查询。
- 精确匹配查询
这种情况下,只需将所要查询的值赋给相应的属性即可。以我们的应用场景为例,当查询公司中所有研发部门的员工信息时,需设置查询条件:{dept : ”研发”}。
- 模糊匹配查询
这种情况下,需要使用通配符来描述所要查询的值。Dojo Data 推荐使用两种通配符:*(任意个数字符)和?(单个字符)
当查询公司中所有 2005 年入职的员工信息时,需设置查询条件:{onboard : ”2005*”}。
对于多条件查询的情况,这时各个查询条件之间是“与”的关系。比如查询公司中所有 2005 年入职的研发部门员工信息时,需设置查询条件:{dept: ”研发”,onboard :”2005*”}。
- 设置查询配置(queryOptions)
在对数据进行查询过滤时,可以使用 queryOptions 来设定一些针对于查询的配置参数。目前,Dojo Data 支持两种配置参数:
- ignoreCase
该参数定义了对数据进行匹配查询时,是否忽略大小写。true 为忽略,false 为不忽略。
- deep
有时我们所查询的数据是分级数据,这时 deep 参数定义了在查询分级数据时,是否要需要查询子结点。true 代表需要,false 代表不需要(这时仅对第一级结点进行查询)。
- 示例
配合 Dojo Data API 的 fetch 方法,我们就可以实现对数据的查询过滤,以下是查询公司中所有 2005 年入职的研发部门员工信息的示例代码:
清单 8 对员工数据进行过滤查询
// 查找 var filter = function(){ request = employeeStore.fetch ({ query: { dept: ' 研发 ', onboard: '2005*' }, queryOptions:{ignoreCase: true}, onComplete: itemsLoaded, start: 0, count: pageSize }); } // 通过 dojo.connect 关联查找操作 dojo.connect(dojo.byId("filter"), "onclick", filter); |
为了使用户能够对数据进行更新操作,Dojo Data 提供了一套 Write API,用来创建、更改、删除数据。同 Read API 类似,Write API 的设计目标也是屏蔽底层数据存储格式的差异,为用户提供统一的数据访问 API。借助这些 API,用户可以专注于业务层面的逻辑实现,而无需花费太多精力去关注底层数据的存储格式。
那么,Dojo Data 是如何实现数据的更新呢?首先,用户利用 Dojo Data 提供的 Write API 对数据进行更新操作,这时,Dojo Data 并没有将这种更新变化传递到服务器端,而是将其保存在本地内存中;其次,Dojo Data 利用 Dojo XMLHttpRequest 技术与服务器进行异步通信,将用户所做的操作传递给服务器端,从而最终实现对服务器端存储文件的更新。遗憾的是,目前很多 service 并不支持数据的更新操作。因此,Dojo Data 的 Write API 的应用不如 Read API 广泛。基于这个原因,我们对具体的 Write API 只作简单的介绍。
- 创建新的数据项
newItem: function(/* Object? */ keywordArgs, /*Object?*/ parentInfo)
- 描述
--- 返回一条新的数据项
--- 参数 keywordArgs:javascript 对象,用来描述新创建数据项的属性
--- 参数 parentInfo:可选。javascript 对象,定义当前创建数据项的父节点
- 示例
清单 9 创建新的数据项
var newEmployee = writeStore.newItem({uid: “001”, name: "Bruce",department: “Sales”}); |
- 更改数据项信息
setValue: function(/* item */ item, /* string */ attribute,/* almost anything */ value)
- 描述
--- 更改数据项中指定属性的值
--- 参数 item:被更改的数据项
--- 参数 attribute:被更改的属性
--- 参数 value:更改后的值
- 示例
清单 10 更新数据项信息
var success = writeStore. setValue (newEmployee, "department", "Support"); |
- 删除数据项
deleteItem: function(/* item */ item)
- 描述
--- 删除某条数据项
--- 参数 item:将被删除的数据项
- 示例
清单 11 删除数据项
var success = writeStore.deleteItem(newEmployee); |
- 保存数据
save: function(/* object */ keywordArgs)
- 描述
--- 将本地的更新操作同步到服务器上去
--- 运行结果将会被传入到参数 keywordArgs 所定义的回调函数中:onComplete: function(),onError: function(errorData)--- 参数 attribute:被更改的属性
- 示例
清单 12 保存数据
writeStore.save({onComplete: onSave, onError: saveFailed}); |
- 数据回滚
revert: function()
- 描述
--- 丢弃没有保存的更新,回滚到上一次保存时的状态
- 示例
清单 13 数据回滚
var success = writeStore.revert() |
本文通过一个具体的应用场景,介绍了 Dojo Data 的使用方法。通过阅读本文读者能够了解 Dojo Data 库的工作原理以及使用 Dojo 提供的存储库来进行统一的数据模型应用开发。