概述
伴随 Web2.0 与 RIA 的快速发展,在客户端处理数据已逐渐成为一种趋势。利用 Dojo Data,Web 开发人员可以在不同的数据格式上建立起一层统一的数据访问模型,使得数据的读写都采用统一的接口,从而降低了客户端逻辑的复杂性,提高了程序的可维护性和可扩展性。在该文章中,我们通过实例具体介绍了 Dojo Data 中 Read API 的技术应用。
Dojo Data Write API 介绍
Dojo 工具包中一些存储库不仅提供了读功能,而且还提供了写功能。也就是说,使用这些存储库,用户不但能够从服务器端获取数据,而且还可以更新服务器端的源数据。这些存储库实现了一套 Dojo Data 所定义的 Write API,用来创建、更改、删除数据。同 Read API 类似,Write API 的设计目标也是屏蔽底层数据存储格式的差异,为用户提供统一的数据访问 API。借助这些 API,用户可以专注于业务层面的逻辑实现,而无需花费太多精力去关注底层数据的存储格式。表 1 列出了 Dojo Data 所定义的 Write API。
表 1.Dojo Data Write API
API | 描述 |
newItem | 创建新的数据项 |
setValue | 更新某个数据项的值 |
deleteItem | 删除某个数据项 |
setValues | 更新一组数据项的值 |
unsetAttribute | 将数据项的某个属性值置空 |
save | 将客户端所做的更新提交到服务器端,完成对源数据的更新 |
revert | 回滚客户端所做的更新,至上一次 save 操作后的状态 |
isDirty | 判断某个数据项是否已在客户端被更新 |
Dojo Data 对数据的更新分为两个阶段。第一个阶段,用户调用 Dojo Data 提供的 Write API 对数据进行更新操作 , 这些操作会被存储库跟踪记录下来。此时这些更新变化还被保存在本地内存中,并没有马上被传送到服务器端。这时如果用户调用 revert 方法,可以放弃这些更新操作,将存储库回滚到上一次调用 save 方法之后的状态;第二个阶段,用户调用 save 方法,利用 Dojo XMLHttpRequest 技术与服务器进行异步通信,将用户所做的操作传递给服务器端,从而最终实现对服务器源数据的更新。这样设计的好处是使得与服务器端的交互可以一次完成大批量的数据更新,也就是批处理操作,并能在客户端支持回滚。Dojo Data 对数据更新的整个过程如图 1 所示。
图 1.Dojo Data 更新操作流程
Dojo Data Notification API 介绍
Web 开发人员都很熟悉 HTML 中的事件机制,onclick、onfocus 等 API 为开发人员实现 Web 应用提供了很大的便利。与 HTML 中的事件机制类似,Dojo Data 也提供了一套事件机制:当我们创建、更改或删除了存储库中的一个数据项时,存储库会抛出一个相关的事件,我们可以捕获这个事件,同时将其与我们定义的方法关联在一起,实现我们自身的逻辑应用。值得一提的是,并不是所有的存储库都提供了 Notification 功能。若要提供 Notification 功能,存储库必须实现 Dojo Data Notification API。表 2 列出了 Dojo Data 所定义的 Notification API。
表 2.Dojo Data Notification API
API | 描述 |
onCreate | 当一个数据项被创建后,该方法将被调用 |
onSet | 当一个数据项被更改后,该方法将被调用 |
onDelete | 当一个数据项被删除后,该方法将被调用 |
那么,如何利用 Dojo Data Notification API 来实现的我们的应用呢?我们可以利用两种方式来将 Dojo Data Notification API 和我们的逻辑实现关联起来。
利用 dojo.connect() 方法来实现关联
清单 1 展示了如果利用 dojo.connect() 方法来实现 Dojo Data Notification API 与具体应用逻辑的关联。
清单 1.利用 dojo.connect() 实现关联
var store = some.NotifyWriteStore();
var alertOnNew = function(item) {
var label = store.getLabel(item);
alert("New item was created: [" + label + "]");
};
dojo.connect(store, "onNew", alertOnNew);
var newItem = store.newItem({foo:"bar"});
重载 Dojo Data Notification API
清单 2 展示了如何重载 Dojo Data Notification API 来实现我们的应用。
清单 2.重载 Dojo Data Notification API 来实现应用
var store = some.NotifyWriteStore();
store.onNew = function(item) {
var label = this.getLabel(item);
alert("New item was created: [" + label + "]");
};
var newItem = store.newItem({foo:"bar"});
借助 Dojo Data 提供的 Notification 机制,我们可以方便快捷的实现很多场景应用。以我们在 Dojo Data 第一部分中的应用场景(企业员工管理系统)为背景,我们将 Notification API(onNew、onSet、onDelete)和刷新操作列表的方法关联在一起,当管理人员操作(创建、修改、删除)员工数据时,用来展示管理员操作记录的文本框将会被自动刷新。在下面的场景应用及示例程序部分,我们会做更为详尽的介绍。
应用场景及示例程序
我们还是以先前 《利用 Dojo Data 开发统一的数据访问模型》一文中的应用场景为例。某公司有 A、B 两个分公司,都保留着各自独立的员工管理系统,而他们又以 Web 服务的方式分别以 JSON 和 XML 的格式来提供数据。我们提供了一个统一的访问入口来分别展现两个分公司的员工信息。
在本文中我们将着重介绍利用 Dojo Data 来实现员工管理系统的增加,删除,修改相关的操作。而这些操作同样都需要服务器端的支持。
服务器端设计
以提供 JSON 数据格式的分公司 A 为例,服务器端通过 JSON 与客户端进行数据交换,其系统架构图如下所示。
图 2. 服务器端架构
服务器端的系统分为三层:HTTP 服务层、业务逻辑层和数据持久层。 HTTP 服务层负责解析客户端的 HTTP 请求,并根据业务逻辑层的处理结果准备响应。业务逻辑层是整个系统的核心,负责权限管理、事务控制以及整个系统的业务逻辑等,数据持久层负责数据的更新、更改,并将其进行持久化。一般说来,各员工的信息存在于数据库中,表 Employee 如下所示:
sn | Name | Sex | dateOfBirth | dept | onBoard | reportMgr |
092334 | 刘君 | 男 | 1966-9-29 | 研发 | 2005-2-20 | 082322 |
099871 | 李丽 | 女 | 1979-8-19 | 销售 | 2000-1-22 | 072121 |
……… | ||||||
……… |
在本文的示例程序中,业务逻辑层省略了权限管理和事务控制的代码部分,仅仅是对数据持久层的简单调用,而考虑到示例代码的简洁性,我们用 employees.json 文件代替了传统的数据库作为员工记录的信息源。其具体做法是:当第一次获取所有员工记录时,我们将整个 employees.json 文件加载到内存中,之后的 add,update,delete 操作修改的只是服务端内存中的数据,而并没有将其持久化到文件系统当中。因此,当服务器重启或者宕机时,之前的修改和更新都会丢失。因此,HTTP 服务层的实现是本文要阐述的服务器端 API 实现的重点。
服务器端 API 实现
Dojo 通过 xhrPost、xhrPut、 xhrDelete 等方法能向服务端发起 HTTP 的 Post、 Put 和 Delete 请求,同样,服务器端通过扩展实现 HTTPServlet 的 doPost、doPut 和 doDelete 方法就能实现对数据的创建、更新和删除。下面将具体阐述其应用场景,我们不会过多的去处理业务逻辑层抛出的异常,因为它们不是本文关注的重点。
创建
当一名新的员工入职时,管理员会在其客户端添加一名新的员工记录,并以 JSON 格式
{sn: '091120', name: '王君', sex: '男' ,dateOfBirth: '1970-7-22', dept: '人力资源', onboard: '2005-1-30', reportMgr: '068972'}
返回,Web 服务层监听到 Post 请求并解析 Employee 的 JSON 格式,将其转换为 Employee 对象,最终调用 EmployService 的 add 方法将其添加到系统的数据库中。其示例代码如下:
清单 3.利用 doPost 方法实现新员工的创建
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
Employee employee;
try {
// 解析请求
String jsonString = EmployeeUtil.getStringFromStream(request
.getInputStream());
// 生成 JSON 对象
JSONObject employeeObject = new JSONObject(jsonString);
// 生成 employee 对象
employee = EmployeeUtil.CreateEmployee(employeeObject);
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
throw e;
}
// 增加员工记录
employeeService.add(employee);
response.setStatus(HttpServletResponse.SC_CREATED);
} catch (Exception e) {
throw new ServletException(e.getMessage());
}
}
当 HTTP 的 Post 请求中不包含有数据或者数据不是一个正确的员工 JSON 格式时,服务端将然会 400 错误,否则返回 201 创建成功。
更新
当一名员工更换部门时,管理员会更改改员工的相关属性,客户端同样应该返回该员工的完整 JSON 格式,例如
{sn: '091120', name: '王君', sex: '男' ,dateOfBirth: '1970-7-22', dept: '人力资源', onboard: '2005-1-30', reportMgr: '068972'}
Web 服务层监听到 Put 请求并解析 Employee 的 JSON 格式,将其转换为 Employee 对象,最终调用 EmployService 的 update 方法将其更新到系统的数据库中。其示例代码如下:
清单 4.利用 doPut 方法实现员工相关属性的更新
public void doPut(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
Employee employee;
try {
// 解析请求
String jsonString = EmployeeUtil.getStringFromStream(request
.getInputStream());
// 生成 JSON 对象
JSONObject employeeObject = new JSONObject(jsonString);
// 生成 employee 对象
employee = EmployeeUtil.CreateEmployee(employeeObject);
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
throw e;
}
// 更新员工记录
employeeService.update(employee);
response.setStatus(HttpServletResponse.SC_OK);
} catch (Exception e) {
throw new ServletException(e.getMessage());
}
}
当 HTTP 的 Put 请求中不包含有数据或者数据不是一个正确的员工 JSON 格式时,服务端将然会 400 错误,否则返回 200 更新成功。
删除
当一名员工离开该公司时,管理员会相应的删除该员工记录,客户端应该提交该员工的 SN, 或者具体的说,请求 URL 为
http://<server name> /employee/567843
Web 服务层监听到 Delete 请求并获取改 Employee 的 SN,调用 EmployService 的 delete 方法,从系统的数据库中移除。其示例代码如下:
清单 5.利用 doDelete 方法实现员工的删除
public void doDelete(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
// 解析请求,获取员工 sn
String sn = (String) request.getParameter(ATTRIBUTE_SN);
if (sn == null || sn.length() == 0) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
} else {
try {
// 根据员工 sn 删除员工记录
employeeService.delete(sn);
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
} catch (Exception e) {
throw new ServletException(e.getMessage());
}
}
}
当 HTTP 的 Delete 请求中不携带有员工的 SN 时,服务端将会返回 400 错误,否则返回 204 删除成功。
dojo.data.ItemFileWriteStore 存储库扩展
dojo.data.ItemFileWriteStore 存储作为 dojo.data.ItemFileReadStore 的一个扩展,它是建立在 dojo.data.ItemFileReadStore 之上的,并支持了 Write API 和 Notification API。因其支持 Write API,dojo.data.ItemFileWriteStore 也提供了一个默认的 save API 实现。但是,因为其并不清楚特定的 service 的实现,这个默认的 save API 实现只是把更新内容提交到内部的存储结构中,并没有真正的提交更新内容到服务器。因而在我们的应用场景中,我们需要根据我们前面 service API 的定义,扩展 dojo.data.ItemFileWriteStore 的 save API。
save 是异步的,因此,save 不会直接返回任何消息内容,save 动作的结果会传递给其他回调函数。一般说来,save 应该将本地数据的变化发送到服务器端已实现数据的持久化,在默认情况下,save 只执行以下动作 :
清除所有已更改的数据,包括新增、修改、删除的数据,这样 isDirty() 将返回 false
提交变化给 DataWriteStore 内部的数据结构
调用传给 DataWriteStore 的回调方法,包括 onComplete 和 onError
由此可见,如果要想将变化持久化,我们必须重写或者扩展 save 方法,重写 save 方法显然要求我们必须了解 DataWriteStore 的内部工作原理,这无疑加大了客户端编程的负担。庆幸的是 dojo.data.DataWriteStore 提供了两个扩展点,即 _saveEverything 和 _saveCustom, _saveEverything 允许我们将整个 datastore 的数据提交给服务器端,而 _saveCustom 允许我们根据程序的逻辑有选择性的提交数据。因此,_saveCustom 成为本示例中要提供的扩展点。Dojo Data 存储库中的 _pending 变量保存了需提交到服务器的内部数据变更集,_pending._newItems 存储了待创建的数据项队列,_pending._modifiedItems 存储了待更新的数据队列,_pending._deletedItems 存储了待删除的数据项队列。通过遍历这些队列,发起相应的 HTTP 请求,便可实现数据在服务器端的更新。其具体代码如下:
清单 6.利用 _saveCustom 实现数据与服务器端的同步
Store._saveCustion= function(sucessHandler, failHandler)
{
// 待创建的数据项队列
var newItems = this._pending._newItems;
// 待更新的数据项队列
var modifiedItems = this._pending._modifiedItems;
// 待删除的数据项队列
var deleteItems = this._pending._deletedItems;
var newItemsOfJson;
var n = 0;
// 遍历待创建的数据项队列,向服务端发起 post 请求,新增员工
for ( var i in newItems) {
var newJsonArrary;
if (!newItemsOfJson) {
newItemsOfJson = {};
newItemsOfJson.identifier = 'sn';
newJsonArrary = [];
newItemsOfJson.items = newJsonArrary;
}
newJsonArrary[n++] = toJsonObject(newItems[i]);
}
if (newItemsOfJson) {
dojo.rawXhrPost( {
url :"employees",
handleAs :"text",
postData :dojo.toJson(newItemsOfJson),
headers : {
"Content-Type" :"application/json"
},
handle : sucessHandler,
error: failHandler
});
}
var modifiedItemsOfJson
n = 0;
// 遍历待更新的数据项队列,向服务端发起 put 请求,更新员工信息
for ( var j in modifiedItems) {
var modifiedJsonArray;
if (!modifiedItemsOfJson) {
modifiedItemsOfJson = {};
modifiedItemsOfJson.identifier = 'sn';
modifiedJsonArray = [];
modifiedItemsOfJson.items = modifiedJsonArray;
}
modifiedJsonArray[n++] = toJsonObject(modifiedItems[j]);
}
if (modifiedItemsOfJson) {
dojo.rawXhrPut( {
url :"employees",
handleAs :"text",
postData :dojo.toJson(modifiedItemsOfJson),
headers : {
"Content-Type" :"application/json"
},
handle : sucessHandler,
error: failHandler
});
}
var deleteItemSns;
n = 0;
// 遍历待删除的数据项队列,向服务端发起 delete 请求,删除员工
for ( var k in deleteItems) {
if (!deleteItemSns) {
deleteItemSns = [];
}
deleteItemSns[n++]=deleteItems[k].sn.toString();
}
if (deleteItemSns) {
dojo.xhrDelete( {
url :"employees?sn="+deleteItemSns.toString(),
handleAs :"text",
handle : sucessHandler,
error: failHandler
});
}
}
客户端设计
在客户端的界面,我们通过表格来展现该公司的员工数据,管理员可以通过添加,修改,删除按钮来更新员工信息。为了达到更好的性能,我们利用了 Dojo Data Write API 的原理特性,每次更新操作并不会实时的对后台服务器数据源进行更新,而只是保存在本地,管理员在做完批量的处理后再点击保存按钮来真正完成数据的更新。这样,大量的数据更新,通过一次函数调用就可完成。当然,根据应用程序的特性,也可以选择在每次更新操作后实时更新到后台服务器。
我们通过 Dojo data Notification API,把用户的每次更新操作都记录显示下来,并由当前管理员决定对已累计的更新操作应用到服务器,或者是回滚取消未应用到服务器的更新操作。员工管理系统用户界面如下图。
图 3. 员工管理系统用户界面
记录每次更新操作
API
onNew: function(newItem, parentInfo)
描述
--- 当在存储库中创建数据项时,该 API 会被调用
--- 参数 newItem:被创建的新数据项
--- 参数 parentInfo:可选,JavaScript 对象。在带有层次结构的存储库中,定义当前所创建数据项的父节点
onSet: function(item, attribute, oldValue, newValue)
描述
--- 当存储库中的任意一个数据项被 setValue, setValues, unsetAttribute 等方法更改时,该 API 会被调用
--- 参数 item:被更改的数据项
--- 参数 attribute item:被更改的数据项属性
--- 参数 oldValue:更改前的数据项属性值
--- 参数 item:更改后的数据项属性值
onDelete: function(deletedItem)
描述
--- 当存储库中的任意一个数据项被删除时,该 API 会被调用
--- 参数 deletedItem:被删除的数据项
示例
在我们的示例中,当用户完成对员工记录的操作(添加、修改、删除)时,左边栏的文本框会显示此次操作的信息。
对于后台实现来说,当 newItem()、setValue()、deleteItem()等 Write API 执行完成后,会自动调用相应的 Notification API。比如,当用户调用 deleteItem()创建新员工记录,待创建完毕后,onDelete()方法会被自动调用执行,将该删除操作的信息写入左边栏的文本框中。具体代码如下:
清单 7.记录删除操作
// 在文本框中显示记录删除操作的结果
publishDeleteInfo = function() {
var operationContent = dojo.byId("operationCont").value;
operationContent += "delete employee record "
+ arguments[0]["sn"] + "\r\n";
dojo.byId("operationCont").value = operationContent;
};
// 将 onDelete 事件关联到处理方法上
dojo.connect(employeeStore, "onDelete", publishDeleteInfo);
图 4. 记录每次更新操作
添加新员工记录
API
创建新的数据项。
newItem: function(/* Object? */ keywordArgs, /*Object?*/ parentInfo)
描述
--- 返回一条新的数据项
--- 参数 keywordArgs:JavaScript 对象,用来描述新创建数据项的属性
--- 参数 parentInfo:可选,JavaScript 对象。在带有层次结构的存储库中,定义当前所创建数据项的父节点
示例
在我们的示例中,用户点击“添加”按钮,弹出 “创建员工记录”对话框,如图 5 所示。用户填写新员工的属性信息后,点击“确定”按钮保存新员工记录。
对于后台实现来说,首先需要创建一个 JavaScript 对象,用来存储新员工的各项属性(姓名、性别、年龄等)及其属性值;之后将该 JavaScript 对象作为参数传递给 newItem 方法,从而完成对新员工数据的创建。
具体代码如下:
清单 8.添加员工记录
// 构建新员工记录数据项
var newEmployeeObj = {
sn :dojo.byId("createSn").value,
name :dojo.byId("createName").value,
sex :dojo.byId("createSex").value,
dateOfBirth :dojo.byId("createBirthday").value,
dept :dojo.byId("createDept").value,
onboard :dojo.byId("createOnboard").value,
reportMgr :dojo.byId("createManager").value
};
// 调用 write API,生成新员工记录
var newEmployeeItem = employeeStore.newItem(newEmployeeObj);
图 5. 添加员工记录
修改员工属性
API
更改数据项信息。
setValue: function(/* item */ item, /* string */ attribute,/* almost anything */ value)
描述
--- 更改数据项中指定属性的值
--- 参数 item:被更改的数据项
--- 参数 attribute:被更改的属性
--- 参数 value:更改后的值
unSetAttribute: function(/* item */ item, /* string */ attribute)
描述
--- 将数据项中指定属性的值置空
--- 参数 item:被更改的数据项
--- 参数 attribute:被更改的属性
示例
在我们的示例中,用户选择一个员工记录,点击“修改”按钮,弹出“修改员工记录”对话框,如图 6 所示,用户填写完相应的信息后,点击“确定”提交更改后的员工属性信息。
对于后台实现来说,我们首先需要获取相应员工的数据项,其次更新改数据项的属性值。因为不能确定用户在界面上更改了哪项属性值,因此程序将依次调用 setValue 方法,将员工的各项属性同时更新。具体代码如下:
清单 9.修改员工记录
if (employeeStore.isItem(items[0])) {
// 获取待更新的员工属性及其属性值
var updateAttr = dojo.byId("updateAttr").value;
var updateValue = dojo.byId("updateValue").value;
items[0][updateAttr]=updateValue;
// 调用 write API,更新员工记录
employeeStore.setValue(items[0], updateAttr, updateValue);
}
图 6. 修改员工记录
删除员工记录
API
删除数据项。
deleteItem: function(/* item */ item)
描述
--- 删除某条数据项
--- 参数 item:将被删除的数据项
示例
在我们的示例中,用户在员工列表中选择待删除的员工记录,之后点击“删除”按钮,确认删除后,将所选中的员工记录删除。
对于后台实现来说,首先需要获得待删除的员工数据项(主要是员工数据项中的 sn 值),之后将其作为参数传递给 deleteItem 方法,从而完成对员工记录的删除。具体代码如下:
清单 10.删除员工记录
if (employeeStore.isItem(items[0])) {
// 调用 write API,删除员工记录
employeeStore.deleteItem(items[0]);
}
图 7. 删除员工记录
保存更新记录到服务器
API
保存数据。
save: function(/* object */ keywordArgs)
描述
--- 将本地的更新操作同步到服务器上去
--- keywordArgs 的定义: {
onComplete: function // 当 save 操作成功时,该函数会被调用
onError: function // 当 save 操作失败时,该函数会被调用
scope: object // 定义 onComplete 和 onError 中语句运行的上下文,默认值是 dojo.global
}
是否为脏数据。
isDirty: function(/* item */ item)
描述
--- 对于所传入的参数 item,判断该 item 是否在上次 save 操作之后被更改,如果已被更改,返回 true,否则返回 false
--- 如果没有传递参数,则 isDirty 会判断存储库中所有的数据项是否在上次 save 操作之后被更改,如果已被更改,返回 true,否则返回 false
--- 该方法一般会在 save()或 revert()操作之前调用
示例
在我们的示例中,当用户在界面上点击“更新”按钮时,用户自上一次 save 操作之后的所做的数据更改都将同步到后台服务器中。
对于后台实现来说,程序会侦听到该按钮的 onclick 事件,并触发 save 方法,将用户的各项更新操作(创建、更改、删除)所带来的数据变化传递给服务器。具体代码如下:
清单 11.保存更新记录
// 提示用户保存成功
var saveDone = function(){
alert("数据更新成功");
}
// 提示用户保存失败
var saveFailed = function(){
alert("数据更新失败");
}
// 当检测到有数据更改时,调用 write API,进行保存操作,将服务器端数据进行同步
if(employeeStore.isDirty()){
employeeStore.save( {
onComplete :saveDone,
onError :saveFailed
});
}
图 8. 保存更新记录到服务器端
数据回滚
API
数据回滚。
revert: function()
描述
--- 丢弃没有保存的更新,回滚到上一次保存时的状态
示例
在我们的示例中,当用户点击“取消”按钮时,用户做的所有数据更改,都将回滚至上次 save 操作后的状态。
对于后台实现来说,程序会侦听到该按钮的 onclick 事件,进而触发 revert 方法,从而回滚用户的更新操作。具体代码如下:
清单 12.回滚
// 当检测到有数据更改时,调用 write API,回滚所做的操作
if(employeeStore.isDirty()){
employeeStore.revert();
}
图 9. 取消更新操作
总结
本文详细介绍了 Dojo Data Write API 和 Notification API 的机制原理,并以 dojo.data.ItemFileWriteStore 存储库为例实现了一个具体应用场景中常见的添加,更新,删除,批处理更新操作。通过阅读本文,读者可以了解 Dojo Data Write API 和 Notification API 的基本原理及使用方法。
除了 dojo.data.ItemFileWriteStore 存储库 , Dojo 工具包还提供了处理基本的 XML 格式数据的 dojox.data.XmlStore存储库。读者也可以根据自己的需求遵循 Dojo Data API 的规范实现处理特定格式的存储库。