面向表格编程的力量
——Butler介绍
原文:http://www.javaworld.com/javaworld/jw-10-2004/jw-1018-butler.html
摘要
自从面向对象和三层结构出现以后,企业应用设计者一直在尝试着隐藏数据库的具体结构。但是这样却增加了软件的复杂性,而且强迫开发者制造了除了底层以外的很多不必要的层次。这篇文章介绍了一个面向表格编程的库——Butler。Butler以JDBC为基础,包括很多具有数据感知能力的SWING组件,可以大大降低在企业客户端应用中书写底层GUI的压力。
当面向对象语言在企业应用中开始应用时,设计者遇到的是如何将关系模型转换成对象模型的压力。在面向对象模型里面,数据是被封装起来的。但关系模型则相反,数据是根本不需要隐藏的。由此很多设计者认为如果使用关系数据库不能被避免,他们将商业逻辑隐藏起来。
面向对象开发者的梦想就是有一个面向对象的数据库。但是这种数据库在市场竞争中无法获得足够的市场份额,而且很可能根本无法取代面向关系的数据库。最近,很多开发者,甚至面向对象论者,都承认关系数据库比面向对象数据要好。
尽管关系数据库有很多优势,但是很多主流的持续性框架(如EJB、JDO 、Hibernate)拒绝在关系数据库里面存储对象。所有的关系数据库都是由表、列、主键、外键、记录和查询等组成的。但是没有一个主流的产品具有符合这些实体的对象模型。
另一个可供选择的模型,面向表格编程模型,允许应用源代码知道真实的数据库结构,而不是将数据库结构隐藏在应用的映射层中。许多企业应用具有很多CRUD(create,read,update,delete)相关的逻辑,如果数据库结构不被隐藏的话,开发CRUD功能将会很简单。
有些人认为对象模型不必和数据库结构一致,这样即使在数据库结构改变的时候商业逻辑也不需要改变。但是这些人忽视了很多商业逻辑是在关系数据库schema上实现的,所以准确的说,改变数据库schema也将改变商业逻辑。
隐藏关系数据库真实结构的结果就是使用一个额外的抽象层。表、列和外键等必须映射到类、属性和关联等。在很多方面,需要为每个表做一个类,把每个列做成类的属性,用关联来代表外键。另外,SQL也被重新改造(如Hibernate查询语言HQL,JDO-QL,EJB-QL等)。为什么要增加这些额外的层次呢?这些层次提供了很少的特性,但是却增加了复杂性。
面向表格编程最大的优势就是提供了创建数据感知GUI组件的能力。如果你正在实现一个用来现实和(或)更新数据库中数据的应用,JTables, JcomboBoxes和JformattedTextFields连接数据库可以节约大量的开发时间。对于Web应用,数据感知标签库也可以加速开发。任何一个主流的持续性框架都比不上数据感知组件这样适合面向表格的框架。
在面向对象的世界里,很多观点都反对数据感知组件。一个常见的观点就是GUI和数据库不能共享同一个结构。一些客户端应用的确是这样的,但是对于面向CRUD的客户端,数据感知组件能节约大量的开发时间。仅仅因为数据感知组件不能在所有应用中使用,不是一个足以反对它的原因。
开发者的另外一个担忧是,害怕使用数据感知组件将对特定的数据库和IDE产生依赖。的确,一些数据库和IDE的开发商制造了一个绑定在特定数据库和IDE上的GUI组件库。但是你如果是具有使用JDBC和ANSI SQL-92语法的组件库,这个库即将和提供商无关。
事实上,对于数据感知组件重要性正在面向对象的世界中慢慢得到重视。甚至SUN公司都已经从开源项目JDesktop Network Components中意识到这种需求。
这篇文章就是讨论一个已经存在的面向表格的持续性框架——Butler。
l 对象模型
Butler具有一系列专门用来描述数据库schema结构的类:
u Table:描述表结构
u Column:代表表中的列
u ForeignKey:两个表之间的外键。
这些类只用来代表数据库的结构(DDL,数据描述语言)。当从数据库中读写内容的时候,值对象也是需要的。什么可以比一个叫做Record的Java类能更自然的代表表中的记录呢?
l 取得记录
通过主键的值来取得记录,你可以调用对应表的实例里面的findByPK()方法,取代从需要的表中使用Select。方法的参数就是主键的值。例如:
Record rec = tableRef.findbyPK(“ABC123”);
如果想取得所有的纪录,只需要调用findAll()方法,这个方法返回记录的列表,而不是一条单独的记录。
对于其他复杂的查询,则必须使用Query类,这个类代表一个SQL中的select声明。很多主流的持续性框架都是使用字符串来代表查询的。在很多方面,用字符串代表使代码紧凑,但是也带来了一些不足。使用对象和方法来构造查询则更加类型安全。大量的错误将在编译的时候被编译器发现,而不是导致运行时错误。IDE的特性,比如代码自动完成,也会使程序员使用Java语言构建查询比使用字符串简单。
Butler中的查询总是具有一个起始表(或者称作主表),这个表代表记录查询的来源。它也可以联合其他的表。结果还将是来自起始表中的记录列表。但是每个来自起始表的记录会和关联的表中记录关联,这些可以通过getRelatedRecord()和getRelatedRecords()方法进行访问。这就相对于JDBC提供了一个很大的不同(或者称作优势),在JDBC中,查询的结果总是二维的。通过记录分层,它还可以被更新或者简单的进行更复杂的结果处理。
Butler使用Filter子类来指定需要选择的记录。例如,EqualsFilter简单的比较提供的值是否和列的值相同。Filter表达用来替代你在SQL声明中所有逻辑操作符。
指定记录排列顺序可以使用addSortCritera()方法。这和SQL中的order by子句对应。
把所有的放在一起,那么一个简单的查询将是:
Query q = orderTable.createQuery();
q.join(orderDetailTable);
q.addColumn(orderTable.getColumn("OrderID"));
q.addColumn(orderTable.getColumn("OrderDate"));
q.addColumn(orderDetailTable.getColumn("ProductID"));
q.addColumn(orderDetailTable.getColumn("Quantity"));
EqualsFilter filter;
filter = new EqualsFilter(orderTable.getColumn("CustomerID"))
q.setFilter(filter);
q.addSortCritera(orderTable.getColumn("OrderDate"));
QueryInstance qi = q.createInstance();
filter.populate(qi, "BLONP");
RecordList recList = qi.run();
Butler对很多类提供内置的XML支持。一个查询结果(RecordList)可以被转换成如下的XML格式:
<records table="Order">
<record>
<column name="OrderID">123</column>
<column name="OrderDate">2004-08-29</column>
<records fk="FK_Order_OrderDetail">
<record>
<column name="ProductID">P01</column>
<column name="Quantity">2</column>
</record>
<record>
<column name="ProductID">P02</column>
<column name="Quantity">4</column>
</record>
</records>
</record>
</records>
这对于生成报表非常有用,可以很方便的转换成用于生成PDF文档的格式化对象,查询结果的层次化结构(相对二维结构)使它成为可能。
l 更新纪录
在Butler中,更新数据库记录非常简单。只需要调用记录实例的set()方法,以列名和值作为参数,来更新你需要的列。接着调用save()方法:
orderRec.set("OrderDate", orderDate);
orderRec.set("ShippingDate", shippingDate);
orderRec.save();
插入记录时首先调用对应的表对象的addRecord()方法,接着设置列的值和保存记录:
orderRec = orderTab.addRecord();
orderRec.set("OrderID", new Integer(123));
orderRec.set("OrderDate", orderDate);
orderRec.save();
删除记录同样简单。只需要调用记录对象的delete()方法。如果你想删除多条记录,则需要使用DeleteQuery类。它和Query类很类似,你需要指定一个filter来代表你需要删除的记录:
deleteQ = orderTab.createDeleteQuery();
filter = new EqualsFilter(orderTable.getColumn("CustomerID"));
deleteQ.setFilter(filter);
QueryInstance qi = q.createInstance();
filter.populate(qi, "BLONP");
qi.run();
Butler还提供了注册记录监视(triggers)的功能,可以给表注册一个RecordListener,在每次更新、插入、删除的之前和之后,将调用对应的事件方法。
l 生成器
如上面的例子一样,列名和表名以字符串的形式给出。程序员可能会键入错误的表名,这将造成运行时错误。为了防止这种情况,Butler可以使用生成器类生成包含每个列的get和set方法的Table和Record子类。
生成器也可以以另外一种形式来使用。产生的子类也包含数据库的结构信息。Butler不必通过JDBC在运行时获得数据的元数据,这被证明对于一些数据库非常耗时。原数据在产生的时候被取得。如果不必生成子类,也可以生成描述数据库结构的XML文件。
l 数据类型
到现在为止,这篇文章已经描述了生成JDBC调用的简单方式。但是Butler也包含了一些JDBC以外的特性。
以数据库为中心的应用总是包含数据的校验和格式化。当用户输入数据的时候,在存入数据库以前必须进行校验。在将数据提供给用户以前,它必须被进行一定的格式化。
在Butler中,每个表中的列都能和一个Datatype对象进行关联。Datatype能校验和格式化列的值。Butler对于字符串、数字值和日期有内值的Datatype实现。但是程序员也可以编写个性化的Datatype类实现处理个性化的数据类型,如电话号码,地址,甚至象图片这样的大数据对象。
上面描述的Butler类可以看作是为了简化和增强数据库编程,而在JDBC上增加的一个层。使用这些类不必改变应用的结构。可以在任何具有JDBC的地方使用。
下面我将介绍数据感知Swing组件。使用这些类需要改变应用的结构,但是数据库结构将不再和客户端不可见。
l Swing组件
在以数据库为核心的客户端应用中,你会发现很多JtableS和数据库表关联的例子。实现这样的表经常耗费很多的时间。但是使用RecordListTable类可以节约不少的时间。程序员只需要告诉数据库中的什么表和列需要显示,以及将RecordList指派给RecordListTable即可。相关记录(多对一关系)中的列也可以被显示。RecordListTable允许更新、插入和删除记录。例如:
recListTable = new RecordListTable(orderTab);
recListTable.addColumn(orderTab.getColumn("OrderID"));
recListTable.addColumn(orderTab.getColumn("CustomerID"));
recListTable.addColumn(orderTab.getForeignKey("FK_Orders_Customers"),
customerTab.getColumn("CompanyName"));
recListTable.recordListSelected(orders);
如果记录的内容需要在RecordListTable以外编辑详细的内容,可以使用RecordEditor类。该类提供一个用来编辑记录的面板,为表中的每列(或者指定特定的列)都提供编辑组件。
RecordEditor实现了ActionListener接口,new命令告诉RecordEditor增加新的纪录,save命令保存当前记录。例如:
editor = new RecordEditor(orderTab);
editor.add(orderTab.getColumn("OrderID"));
editor.add(orderTab.getColumn("OrderDate"));
saveButton = new JButton("Save");
saveButton.setActionCommand("save");
saveButton.addActionListener(editor);
为了个性化层次,可以使用RecordController代替RecordEditor。不可见的组件将自动产生。RecordController有一个用来产生编辑组件的工厂方法。程序员只需要将这些组件放在需要的位置:
controller = new RecordController(orderTab);
ValueEditor editor;
Column col = orderTab.getColumn("OrderDate");
editor = controller.createColumn(col)
panel.add(editor);
对于RecordEditor来说,RecordControler的工作方式和RecordSelectionListener类似。
在很多客户端中,已有记录的列表(RecordListTable),在一行(记录)被选中时,选中记录应该在一个详细视图(RecordEditor或者RecordController)中显示。为了在Butler中增加这种功能,只需要将RecordEditor以RecordSelectionListener的形式注册给RecordListTable:
recordListTable.addRecordListTable(recordEditor);
在很多客户端中,用户需要首先选中他需要显示或者编辑的记录的能力。在Butler中,可以使用QueryPanel来增加这种能力,QueryPanel获得一个Query后将产生一个对应的GUI,可以在里面输入过滤器获得的值。如果结果需要在RecordListTable中显示,只需要将RecordListTable以RecordSelectionListener的形式注册给QueryPanel:
query = orderTab.createQuery();
query.setFilter(new EqualsFilter(orderTab.getColumn("CustomerID")));
panel = new QueryPanel(query);
recListTable = new RecordListTable(orderTab);
panel.addRecordSelectionListener(recListTable;
如果需要个性化的层次,可以使用和QueryPanel对应的QueryController。
上面介绍的是一些常用的数据感知组件,如果你观察一个企业客户端应用,你会在很多画面中发现以下的模式:在应用画面的上面是用来输入查询参数和查询按钮的部分(QueryPanel),下面是用来显示查询结果的列表或者表格(RecordListTable),再下面,是一个类似弹出窗口的用来编辑记录的详细视图(RecordEditor)。
如果你需要创建这样模式的客户端,同时你不需要个性化层次,你不必手动设计这些组件。相反,使用SimpleForm类创建这样结构的画面,只需要几行代码。
l 总结
这篇文章描述了非主流的数据库编程解决方案。这种方案类似面向对象出现以前的数据库编程。但是最大的不同是:Butler利用了面向对象编程的完整力量,使关系数据库和面向对象的编程语言不存在任何不协调。
相关网址:
Butler数据库框架:http://butler.sourceforge.net
面向表格编程的更多信息:http://www.geocities.com/tablizer/top.htm
(第一次翻译,不足之处大家海涵。cqucyf@263.net)