一个轻量级数据管理与分析平台的实现
2009-11-05 00:00:00 来源:WEB开发网应用背景简介
在许多典型行业应用中,需要处理按照地域、时间或类别等维度产生并被管理与维护的数据。被管理的数据分类方式基本维持恒定,而数据本身的内容(字段)则需要根据业务需要不断变化。一方面,被管理的数据必须支持按照权限进行增删改查操作;另一方面,还需要能够进行所见即所得的图表分析,如不同地域之间数据的对比、时间维度数据变化规律的分析、不同类别数据的分类汇总、甚至包括关联数据之间的统计关系维持等。
例如在电力行业,电网公司按照地域进行划分,其以电量、负荷为代表的核心业务数据按照时间发生并被计量。这些被管理的数据具有一定的共性:数据所属地域、数据的时间划分、数据的统计口径等数据的维度基本维持不变,但数据的类别却随着经济形势发展变化、不同地区产业结构、甚至是地方政策等经常发生变化。以地区的月度用电量为例,虽然电力行业有用电分类标准用以统一各地区的用电分类和类别名称,但是经济发展的不均衡和产业结构的差异,使得不同地区实际需要关注的用电分类大同小异,这要求系统提供的数据管理分析功能能够对后台关系数据库中数据表列的变化有较强的适应能力;另一方面,地区差异性还导致了不同电网公司关注的数据集合本身就有差别,这又要求系统提供的数据管理分析功能能够适应增减数据库表的个性化需求。
暂且把本文需要实现的目标功能称为数据管理分析平台,它需要具备如下特点:
这是一个用在产品中的功能,通用性和可维护性是第一位的。
这是一个 B/S 应用,用户使用浏览器访问系统。
用户在界面上可以对数据进行增删改查。
不同数据之间的计算关系维持对用户透明。
用户可以对查询到的数据进行作图分析。
跨数据库平台。
分析与方案选型
由于期望这个数据管理分析平台能够在不同的产品中复用,因此本方案应针对数据存储在关系数据库中的共同特征进行抽象,而不是针对特定产品的业务逻辑进行抽象。换句话说,应该针对关系数据库的表和字段进行抽象,而不是根据特定产品的业务数据对象进行抽象。这允许最终实现能够通过简单配置来实现增加和减少被管理数据类别(被管理表的扩展),以及对特定数据类别的增加和减少被管理数据字段(被管理列的扩展)。
为何使用 JDBC
如果把被管理的数据,按照数据库表映射为 POJO,再针对 POJO 实现后续的展现和 Persistence 操作,即使 DIY 出一个像 Hibernate 一样完整的 Persistence 工具来,需要增加被管理的数据类别时,仍然需要根据这个数据类别对应的关系数据库表,映射出一个 POJO 类。这个过程产生了新的 Java 代码,意味着研发、测试、发布、实施等整个软件工程过程被启动了,把这个过程叫做配置是明显不合适的。
当然,B/S 架构应用中常用的通过表单提交方式实现 CRUD 的方案就更不可取了,以流行的 SSH(Struts、Spring、Hibernate)模式为例,增加新的被维护数据,意味着至少需要增加一个 JSP 页面用来生成用户界面,一个 ActionFormBean 用来接收表单的数据,一个 Hibernate 的表映射 POJO 对象,不仅同样意味着启动一个完整的软件工程工程,而且工作量是随着新增数据类别的数目线性增长的。
说到这里,可能您已经想到了数据库客户端,没错,数据库客户端就是一个典型的所见即所得满足用户对数据进行增删改查需求的实现。如果我们对一个数据库客户端进行改造,让它将查询结果以用户的业务视角进行展现,同时让它在浏览器中运行,提供对所显示数据进行作图分析的功能,并且能够维持关联数据的计算关系,那就圆满了。
可惜的是,我们不能像数据库客户端那样,让最终用户直接面临存储业务数据的物理表,在一个数据库表的直接查询结果上进行增删改查操作。因为在满足数据库设计范式的前提下,数据表中有很多我们称之为 ID 的字段。例如,一个按地区存储某种数据的表中,表示地区的字段中,一定是存储了一个代表地区的 ID 和另一个存储地区信息的数据表做外键关联,在查询时,需要做多表连接才能获得地区 ID 对应的可以让最终用户读懂的地区描述信息。
因此,我们的数据管理分析平台,至少应该有能力向用户提供一个可以修改的多表连接查询结果集。如果你想到了视图,那很好,因为确实很多关系数据库都支持视图的更新,而且自打 JDK1.4 以后,JDBC 的 RowSet 扩展也正式成为了 JDK 标准 API 的一部分,允许通过 JDBC 对查询结果集进行数据更新操作。如果我们的数据管理分析平台是绑定在特定数据库平台之上的,那么仔细研究一下对应的数据库平台对视图更新的支持情况,仔细规约出一套可行性视图设计方案也许理论上是可能的。但是,我们的数据管理分析平台是希望能够跨数据库平台的,由于不同的数据库在视图更新上各有特点,采用这个方案,将会大大降低解决方案的数据库无关性。
既然如此,不妨抱住 JDBC 的大腿再好好想想。在 Java 世界里,我们使用 JDBC 来完成对一行数据的增删改查操作,有两个必要条件:一是能获得数据表的名称,二是唯一定位数据行的条件,通常是主键值。因此,对于一个连接查询的结果集,只要能够确认被修改数据列对应的表,以及该列所在行对应的主键值,使用标准 SQL,借助 JDBC 就能实现数据的更新操作。
以国家统计局的年度人口统计数据为例,用户看到的展现形式如下:
表 1. 年度人口数据的用户界面
地区 | 年份 | 总人口 | 男 | 女 | 城镇 | 乡村 |
全国 | 1999 | 125786 | 64692 | 61094 | 43748 | 82038 |
全国 | 2000 | 126743 | 65437 | 61306 | 45906 | 80837 |
全国 | 2001 | 127627 | 65672 | 61955 | 48064 | 79563 |
全国 | 2002 | 128453 | 66115 | 62338 | 50212 | 78241 |
全国 | 2003 | 129227 | 66556 | 62671 | 52376 | 76851 |
全国 | 2004 | 129988 | 66976 | 63012 | 54283 | 75705 |
全国 | 2005 | 130756 | 67375 | 63381 | 56212 | 74544 |
全国 | 2006 | 131448 | 67728 | 63720 | 57706 | 73742 |
全国 | 2007 | 132129 | 68048 | 64081 | 59379 | 72750 |
这里的数据,应该来自关系数据库中的两个表,它们是:
表 2. 地区表(AREA)
Name | Type | Comments |
AREAID | VARCHAR2(10) | 地区 ID,主键 |
AREANAME | VARCHAR2(20) | 地区名称 |
表 3. 人口表(POPULATION)
Name | Type | Comments |
AREAID | VARCHAR2(10) | 地区 ID,主键,外键 |
YEAR | VARCHAR2(4) | 年份,主键 |
TOTAL | NUMBER | 总人口 |
MEN | NUMBER | 男 |
WOMEN | NUMBER | 女 |
CITY | NUMBER | 城镇 |
COUNTRY | NUMBER | 乡村 |
表 1 所示的数据,应该来自表 2 和表 3 的连接查询:
代码 1. SQL 示例Select
A.AREANAME,B.AREAID,B.YEAR,B.TOTAL,B.MEN,B.WOMEN,B.CITY,B.COUNTRY
from
AREA A, POPULATION B
where
A.AREAID = B.AREAID
如前所述,查询结果集中包含了 AREA 表的 AREANAME 列,满足了用户业务逻辑视角的展示需要;包含了 POPULATION 表的 AREAID 和 YEAR 两列,满足了在 POPULATION 表中定位数据行进行数据更新的需要。
采用 Applet + JSP
如果仅仅只需要满足表 1 所示的图形界面——一个展现数据的表格,那么表现层的选择没有任何约束,可以用在 Java EE 平台下的任何表现层技术都能满足。表格中的数据需要能够修改;如地区这样的列需支持下拉列表选择填写;表格中的数据可以支持复制、粘贴;必须允许用户根据表格中的数据制作图表;图表可以放大缩小;图表可以打印;图表可以导出成图片;……
随着要求的进一步增多,可选范围迅速减小,但是可以肯定的是,Java Swing + JFreeChart 差不多可以满足所有要求。考虑到这个功能是用于 B/S 架构系统中的,选择嵌入 JSP 页面的 Applet
作为表现层策略,是合适的。
一旦表现层确定为 JSP + Applet 的方式,意味着客户端获取到了最大的可交互性。在服务器端查询的结果集,进行简单封装,采用对象序列化方式,将其传送到 Applet 端,Applet 端使用 Java Swing 构造显示的图形界面,并处理用户的操作。
使用模板 SQL 实现查询
在用户界面中,像时间和地区这样的数据筛选条件,可能分别用下拉列表选择和树形结构实现,以增强直观度和易用性。换句话说,代码 1 所示的 SQL 语句中,需要根据用户在界面上的选择结果,生成适当的 where 子句以响应筛选条件。假设根据需求,构造了如下图所示的 UI:
图 1. UI 示意图
图片看不清楚?请点击这里查看原图(大图)。
那么,图示的 UI 中,地区的选择,在代码 1 对应的 SQL 中,对应添加 B.AREAID=? 的 where 子句选;两个年份的选择,对应添加 B.YEAR between ? and ? 的子句;而数据类别的选择,则是用来确定被查询的数据类别的。考虑使用如代码 2 所示的 SQL 语句:
代码 2. SQL 示例Select
A.AREANAME,B.AREAID,B.YEAR,B.TOTAL,B.MEN,B.WOMEN,B.CITY,B.COUNTRY
from
AREA A, POPULATION B
where
A.AREAID = B.AREAID and B.@area and B.@year
上述代码中,@area 和 @year 是需要根据用户所选择之条件进行替换的替换标识。
不要小看这一点点小手段,统计局的大多数数据都是按照地区和时间进行分类和统计的,因此上面的简单方案已经可以处理大多数统计数据的查询需求了。而且 @area 这个替换标识,既可以用 AREAID=? 的子句替换,也可以用 AREAID in (?,?) 的子句替换。对应到操作中,就是可以实现不同地区数据的对比查询,再使用 JFreeChart 提供图表支持,就可以实现一系列有价值的数据分析功能。
另外,代码 2 的 SQL 示例查询的结果集中,POPULATION 表的 AREAID 字段虽然是查询结果集的一部分,但是这个字段并不应该显示在最终用户界面中,查询出这个字段是为了满足结果集可更新的必要条件。因此,可以提供一个额外的配置信息,指明查询结果集中的哪些字段在传输到客户端 Applet 后需要被“隐藏”起来,客户端 Applet 处理显示时,不显示这些字段,但需维持这些字段和数据行之间的关联关系不被破坏。
具体的实现细节,请参见本文附带的参考实现代码。
记录用户的增删改操作
如何实现查询操作,前面的小节已经表述了,本节的核心是解决用户的数据修改如何保存到数据库中。
首先重复一下对关系数据库中的一行记录进行修改的两个必要条件:一是能获得数据表的名称,二是唯一定位数据行的条件,通常是主键值。通过上一节描述的模板 SQL 中查询列的约定,可以确保目标数据表的主键列都被查询到数据结果集中,并被封装后传送到客户端 Applet 以供显示。因此,对数据增删改支持的实现,其实就是需要实现记录用户的增删改操作,并由服务器端根据该记录生成系列 SQL 语句并执行。
修改操作,可以分为两种情况,一种情况是修改非主键数据列,另一种情况是修改主键数据列。前者是安全的,也是容易记录的;后者则会带来一些问题:一是主键重复问题,不过这个无法由程序替用户解决,二是主键被修改后,相当于数据行的唯一定位条件丢失,如果不能找回原来的主键值,则无法完成相应的 Update 操作,还可能更新错误的数据行。
删除操作,也有两种情况,一种情况是删除时,该行数据的主键列未被修改,另一种情况时,删除操作时,主键列已经被修改过了。结合修改情况考虑,也会出现类似的情况,由于主键列已经被修改而造成删除操作无效(主键对应的记录不存在)或者删除了错误的记录(主键修改后,与另一条已经存在的数据记录一致)。
对于增加,情况稍微复杂一点,一来是像表 1 所示的地区列,是属于外键关联数据,要求用户记住描述字段来填写是不合理的,理应提供下拉列表供用户选择;二来数据表中可能有标识性的 ID 字段作为主键,例如自动增加的 ID 主键,可能由数据库维护,也可能由程序维护,需要特别处理。考虑到需要适应不同数据库平台的限制,保险起见,可以约定都使用程序维护。
维持关联数据的一致性
一旦涉及到数据修改,就有可能涉及到数据一致性问题,例如前面列举的 POPULATION 表记录了人口数据,本文附件的参考实现中,还有一个 ECONOMIC 表记录了经济数据,其中的人均 GDP 数据是和 POPULATION 表中的总人口以及 ECONOMIC 表中的 GDP 有逻辑计算关系的。如果用户修改了总人口数据,那么人均 GDP 理应一并被修改,如果用户修改了 GDP 总值数据,那么人均 GDP 同样也应重新计算。
数据库触发器理论上是一种选择,但在本文的场景下,它至少有两方面缺点。首先,它是数据库平台相关的,编写的触发器和存储过程有移植代价;其次,数据之间的计算关系,属于业务逻辑,在当前分层而治的主流架构思想下,在数据库中处理部分业务逻辑,这属于业务逻辑的不合理蔓延。
只要换种思路,事情就变得简单了。维持关联数据一致性的逻辑,在 Java 类中实现;当对应的数据被修改后,只要能够出发相应 Java 类的调用就行了。对于图 1 所示的一个被管理的数据类别,数据被修改后,可能出发的关联数据计算是既定的。在配置时,增加一个新的配置项,即数据修改后需要调用的统计实现 Java 类就可以了。配置项的内容可以直接使用 Java 的类名,约定提供默认无参数构造函数,实现统一接口,便于进行 reflect 调用就行了。
架构设计
根据前面章节的分析,确定使用 Applet 作为表现端,服务器端则采用 Servlet 与 Applet 进行通信,接收来自 Applet 的用户请求,并将处理后的结果返回给客户端。如下图所示:
图 2. 架构简图
其中,Applet 和 Servlet 之间的通信使用 java.net.URLConnection 和对象序列化实现,Servlet 和 Database 当然是通过 JDBC 执行 SQL 语句来完成交互。
图 3. 类图
参考实现说明
有兴致的读者可以下载本文附带的附件,MellonDataManager.jar,它包含一个由 NetBeans6.5 打包的可执行 jar 文件:DataAnalyse.jar。还有一个 lib 目录,里面应该放置示例代码的执行所需要的 3 个 jar 文件,derby.jar(Java DB),可以在 JDK1.6 中获得,也可以到 http://developers.sun.com/javadb/downloads/index.jsp 下载;jcommon-1.0.16.jar(JFreeChart 需要的支持包)和 jfreechart-1.0.13.jar,这两个包可以到 http://www.jfree.org/jfreechart/download.html 下载。以及一个 src 目录,是所有的源文件。
cn.mellon.Database 类,负责初始化数据库和提供数据库连接。
cn.mellon.FakeServlet 类,因为本文所描述的应用应该是一个 Web Application,为了使程序能够作为独立 Application 执行,使用本类伪装 Servlet 来接收 Applet 发送的请求,其实是 Applet 直接在同一个 JVM 中调用的,不过将其改为 Servlet,并让整个应用运行在 Tomcat 中,也是非常容易的事情。
cn.mellon.DataManager 类,是核心的 Applet 实现,同样的,为了方便程序作为独立 Application 运行,这个 Applet 被添加了 main 方法,因此它兼具 Applet 和 Application 双重身份。
当您下载到三个必须的 jar 包,并将起放置在指定的 lib 目录后,您可以使用命令行 java – jar DataAnalyse.jar 来运行这个示范实现,程序首先初始化数据库,创建几个示例的表并未对应表插入一定数量的数据。
程序的运行效果,如下图所示:
图 4. 参考实现运行效果
对表格中数据的修改和删除操作,都记录在 cn.mellon.DMDataWrapper 类中,cn.mellon.FakeServlet 通过解析这个类,生成相应的 SQL 语句后执行,从而将数据修改更新到数据库中。
图 3 的界面中,左上方的地区选择树内容来自地区表,可以修改地区表对应的数据后,重新启动程序观察效果。配置表是本功能实现的核心配置数据所在,本参考实现也一并将其暴露以允许在界面中修改配置信息,观察效果。同样的,修改保存后,需要重新启动程序,方能看到效果。因为配置信息和地区信息一样,都是在 Applet 加载时,从服务器端获取并被缓存在 Applet 端的。
还有一些和本文主题关系不大的实现细节,读者可以从代码或注释中阅读到,如果想进一步交流,可以发邮件给我。
优势与局限性分析
本文所述的方案是应用于 Web Application 的,对于经常需要添加新的数据类别的应用,可以免去针对每种新增数据重复开发对应的前端页面和后台处理类等服务器端程序。通过简单配置即可对新增的数据表对应的数据类别完成增删改查系列操作,也同时获得了相关的图表分析功能,相关的分析功能越多,这个配置产生的收益就越大。
本方案也有局限性,由于本方案是基于 JDBC 的,因此当本方案和基于 O/R Mapping 实现 Persistence 层的 Web Application 共存时,与 O/R Mapping 的缓存机制恐无法兼容,因为本方案相当于不经 O/R Mapping 层直接修改了数据库表。
另外,本方案的数据被查询出来后,处在离线状态,不同用户在同一时间对相同数据的修改动作,是相互覆盖的,至少在服务器端实现同步处理之前是这样的。如果使用系统的用户数量有限,且各自维护的数据没有重叠,基本上是安全的。如果使用系统的用户数量很大,彼此数据重叠,则需要考虑控制同步问题,很明显本方案是不适合用在并发访问数量非常大的匿名访问系统中的。
本文示例源代码或素材下载
更多精彩
赞助商链接