普适数据容器
数据集(DataSet)是一个普适的,弱类型的,开放的数据容器。普适是指它可以适应任何基于名值对集合或名值表的数据形式,比如Properties,Database Tables, XML, CSV, JSON, LDAP, ...。弱类型是指所有的容器都用同一个数据结构,存放在容器中的只有两种对象:数值和子结构。数值一律用字符串表示,时间日期用XML标准形式来表示,子结构一律用数据集表示。有关定义域的元数据存放在专门的数据结构中。如果需要类型转换,借助元数据,可以很容易地做到。所以为了突出本质,我宁愿把数据转换的代码放到AccessorWrapper里。事实上在我们的项目里,根本就没有用到类型转换。所谓开放,就是前面说过的,容器可以适应数据模型的变化,以不变应万变。
下面给出两个主要的接口。DataSet继承Iterator和Map,即是为了表明概念继承上的渊源,也是为了与Web模板结合。数据集在元数据只含有字段名,并且不具有滚动性质的情形下,退化为Map。
java 代码
- public interface DataSet extends Iterator, Map
- {
- public abstract String getDataSetName();
- public abstract void setDataSetName(String name);
- public abstract DataSetMetaData getMetaData();
- public abstract void setMetaData(DataSetMetaData md);
- public abstract DataSet getDataSet(int column);
- public abstract void setDataSet(int column, DataSet ds);
- public abstract DataSet getDataSet(String path);
- public abstract void setDataSet(String path, DataSet ds);
- public abstract String getValue(int column);
- public abstract void setValue(int column, String value);
- public abstract String getValue(String path);
- public abstract void setValue(String path, String value);
- }
- public interface DataSetMetaData
- {
- public abstract int getColumnCount();
- public abstract void setColumnCount(int count);
- public abstract int getColumnDisplaySize(int column); // optional
- public abstract String getColumnLabel(int column); // optional
- public abstract String getColumnName(int column);
- public abstract int getColumnType(int column);
- public abstract String getColumnTypeName(int column);
- public abstract int getPrecision(int column); // optional
- public abstract int getScale(int column); // optional
- public abstract boolean isAutoIncrement(int column); // default "no"
- public abstract boolean isCaseSensitive(int column); // default "yes"
- public abstract int isNullable(int column); // default "yes"
- public abstract void setAutoIncrement(int columnIndex, boolean property);
- public abstract void setCaseSensitive(int columnIndex, boolean property);
- public abstract void setColumnDisplaySize(int columnIndex, int size);
- public abstract void setColumnLabel(int columnIndex, String label);
- public abstract void setColumnName(int columnIndex, String columnName);
- public abstract void setColumnType(int columnIndex, int DataType);
- public abstract void setColumnTypeName(int columnIndex, String typeName);
- public abstract void setNullable(int columnIndex, int property);
- public abstract void setPrecision(int columnIndex, int precision);
- public abstract void setScale(int columnIndex, int scale);
- }
数据集的辅助工具
为了便于应用,需要有一整套的工具类。下面列出与数据集应用有关的核心类和辅助类,结合后面的用例,可以让你对数据集的应用方式有一个大概的了解。核心接口
DataSet
DataSetMetaData
DataSetException
...
核心实现:
DataSetBaseImpl
DataSetMetaDataBaseImpl
DataType
TypeFormat
...
辅助工具包:
DataReader < org.xml.sax.XMLReader
DefaultReader <~ JAXP Default Reader
DataSetReader
DBTableReader
DBLobReader
JndiReader
TeeFilter
ObjectReader
ResultSetReader
...
DataSource < org.xml.sax.InputSource
DataSink ~ org.apache.xml.serializer.Serializer
DefaultSink ~ org.apache.xml.serializer.Serializer
DataSetSink
DBTableSink
DBLobSink
ObjectSink
DataPipeFactory
IO扩展包
InputStreamBuffer, OutputStreamBuffer
NullInputStream, NullOutputStream
ByteMessageInputStream, ByteMessageOutputStream
TextMessageInputStream, TextMessageOutputStream
SmartZipInputStream, SmartZipOutputStream
RewindableInputStream
FanOutputStream
StreamCopier
组件框架
略过
数据集用例
在应用上,数据集的应用模式,遵循了SAX 2的标准模式。SAX 2的标准模式
java 代码
- XMLReader reader = org.xml.sax.helpers.XMLReaderFactory.createXMLReader();
- //获取一个InputStream实例is,然后
- InputSource source = new InputSource(is);
- //获取一个ContentHandler实例handler,然后
- reader.setContentHandler(handler);
- reader.parse(source);
数据集的例子
java 代码
- EndPoint source_ep = new EndPoint("jdbc://MyDataSource/MySchema/MyName");
- DataReader reader = DataPipeFactory.createDataReader(source_ep);
- DataSource source = DataPipeFactory.createDataSource(source_ep);
- EndPoint sink_ep = new EndPoint("object:///net/hyperdigital/dataset/DataSet");
- DataSink sink = DataPipeFactory.createDataSource(sink_ep);
- DataHandler handler = sink.asContentHandler();
- reader.setContentHandler(handler);
- reader.parse(source);
- DataSet ds = handler.getContent();
上面这段代码将执行“select * from MySchema.MyName",并将数据读入一个数据集,数据集的名字是MyName。如果我想执行一个特定的SQL语句,我只要在上面第一行后加上一行:
java 代码
- source_ep.setProperty("read", "select xxx,xxx, xxx from xxx where ...");
相应的代码会执行给定的SELECT语句,并把结果读入一个数据集,数据集的的名字属性是MyName。
如果我想把结果存放在一个XML文件中,可以这样做:
java 代码
- EndPoint source_ep = new EndPoint("jdbc://MyDataSource/MySchema/MyName");
- DataReader reader = DataPipeFactory.createDataReader(source_ep);
- DataSource source = DataPipeFactory.createDataSource(source_ep);
- EndPoint sink_ep = new EndPoint("file:///absolution/file/path/MyFileName.xml");
- DataSink sink = DataPipeFactory.createDataSource(sink_ep);
- DataHandler handler = sink.asContentHandler();
- reader.setContentHandler(handler);
- reader.parse(source);
和前面的代码相比,不同的仅仅是数据汇(数据流向的终点)的定义。如果要把数据存作XML并且发到消息队列上,同样只要改动数据汇:
java 代码
- EndPoint sink_ep = new EndPoint("queue://MyQCF/MyQueue");
XML是假定的媒介形式,我可以以通过指定端点的属性来改变媒介形式。
如果我要把数据存入数据库表,假定数据集里的字段名和数据库表中的字段名相同,(convention over configuration),相应的代码如下:
java 代码
- EndPoint source_ep = new EndPoint("object://java/net/hyperdigital/dataset/DataSet");
- DataReader reader = DataPipeFactory.createDataReader(source_ep);
- DataSource source = DataPipeFactory.createDataSource(source_ep, dataset);
- EndPoint sing_ep = new EndPoint("jdbc://MyDataSource/MySchema/MyTable");
- DataSink sink = DataPipeFactory.createDataSource(sink_ep);
- DataHandler handler = sink.asContentHandler();
- reader.setContentHandler(handler);
- reader.parse(source);
数据迁移是小菜一碟:
java 代码
- EndPoint sing_ep = new EndPoint("jdbc://MyDataSource/MySchema/MyTable");
- DataReader reader = DataPipeFactory.createDataReader(source_ep);
- DataSource source = DataPipeFactory.createDataSource(source_ep, dataset);
- EndPoint sing_ep = new EndPoint("jdbc://YourDataSource/YourSchema/YourTable");
- DataSink sink = DataPipeFactory.createDataSource(sink_ep);
- DataHandler handler = sink.asContentHandler();
- reader.setContentHandler(handler);
- reader.parse(source);
不厌其烦贴了那么多,是为了显示在发布系统中任意两个端点间的数据通讯,都是同一模式。现在让我们来看一看EndPoint是个什么东西。java.net.URI是不可继承的,所以下面extends的意思是EndPoint包括URI的所有方法。
java 代码
- public class EndPoint extends URI
- {
- public EndPoint(String endpoint) {}
- public EndPoint(String context, String relative) {}
- public EndPoint(URI uri) {}
- public EndPoint(URI context, String relative) {}
- public String getContextPath() {}
- public String getRelative() {}
- public String getTextType() {}
- public URI getURI() {}
- public String getProperty(String name) {}
- public void setProperty(String name, String property) {}
- public Set getPropertyNames() {}
- }
主要的属性(Property)有方法(method)和文本类型(text_type)。方法可以是Read, Write, Append, Delete, Get == Reader, Insert == Put == Write, Post == Update。Read是假定的也是唯一的源方法,Write是假定的汇方法。文本类型的值可以是text, xml, html,这些是Xalan中已经实现的,也可以很容易地增加cvs, json等等类型。EndPoint里的URI,基本上都是URL。EndPoint支持的传输协议,包括所有java.netURLConnection支持的协议,再加上jdbc(数据库表和LOB),jndi, queue,topic,object。Object的主要支持字符串、数据集、Map、。。。,字符串里的内容,可以是任何文本类型(text_type)。
再回过头去看上面的代码片断,设想一下各种数据源和数据汇的组合,上面的代码,远远超过了仅仅作数据库访问的意义,它可以用于网络上任意两个组件或者服务之间的数据传输,它可以看成是一个分布式数据访问和数据传输的框架。
数据集的应用模式
对数据的操作可以有两种模式:一种是OO模式,另一种我暂且称之为数据集管道模式,是我要介绍的模式。在管道模式看来,数据集(DataSet)代表一个向量或者一个向量的序列。那么,对数据的操作就是作用于这个向量的一个矩阵,而ContentHandler就是一个事件驱动的矩阵实现。普通的ContentHandler更适于同一空间中的线性变换(实际上不一定线性),而Transformer对应于不同空间之间的变换。数学上,矩阵是可以连乘的,在SAX 2,Transformer是可以像管道一样串连的,二者一一对应。这种管道化的特性,提供了又一个分解复杂逻辑的手段,使我们能够把复杂逻辑分步骤实现,每一个子逻辑块实现一个明确的子目标。
数据集的管道模式借用了SAX 2的管道模式。上面的几个例子显示了数据传输的应用,它们是最短的管道系统,一个输入缓冲池(DataSource),一个泵(DataReader),一个输出缓冲池(DataSink)。系统把流体(数据)从输入缓冲池运送到输出缓冲池,不做任何处理。如果要对数据进行处理,只要在泵和输出缓冲池之间接上一个或几个过滤器(XMLFilter/Transformer)就可以了。过滤器可以是自己实现的ContentHandler,也可以是由XSLT产生的Transformer。
管道模式实际上就是用XML Schema实现领域模型,用ContentHandler/XSLT实现领域逻辑的模式。管道模式的好处是:1。最弱的类型依赖,所有的数值都是字符串类型,程序员不必费心类型转换。2。最佳的动态性能。XML Schema和XSLT都是文本文件,可以动态部署,动态修改。领域模型和领域逻辑的改变后,代码不必跟着修改。坏处是你得学XML和SAX 2。较之于一大堆繁不胜繁,遮遮掩掩,修修补补而又欲盖弥彰的设计模式起来,XML要简单直白多了。另一个坏处是,XSLT的功能不够强大,不足以应付在某些复杂情况。不过,某些OO模型下看来很复杂的问题,用XSLT来实现的话,也可能很简单。
OO模式也可以由两种做法。一种是传统的做法。一种是在数据集的基础上构造领域模型,将领域模型封闭在领域逻辑层。传统的领域模型+ORM的模式,不可避免地会把领域模型的细节暴露给领域逻辑层以外,而造成系统对领域模型不必要的依赖,大大降低系统的稳定性和动态性。我对传统的领域模型+ORM产生的大量的空壳类和映射文件十分厌恶,在我看来,它们就像一具具没有灵魂,没有血肉的骷髅,可以用尸横遍野来形容。
从对数据所负的责任来看,系统的参与者(actor)可以分为两类。一类是中介(agent),一类是客户(client)。客户是数据的最终消费者,负责对数据的解读和处理加工。数据只有对客户才有逻辑的和物理的意义。而中介只是传递数据,数据对于中介来说是没有意义的毛数据(raw data),中介的责任是将毛数据不失真的传递到客户的手中。从系统的壳层(layer)结构来看,客户位于领域逻辑层内部而中介位于领域逻辑层外部,因此中介们不必要也不应该知道领域模型的细节。这才符合编程的基本道理。通常的Domain Model+ORM的模式,就是因为违背了这个基本的道理,才有了种种的弊端。
小结
- 数据集是一个普适的,开放的数据容器。数据集可以用来承载任何基于名值对的数据媒介。
- 在数据集中,数值和元数据是分离的。元数据,除了字段名以外,可以在需要的时候延迟加载,不需要就不必加载。
- 用数据集构造的系统,在领域逻辑层外,数据被视为毛数据,不再有类型的桎梏。
- 在纵向,数据集可以应用于系统的各层之间,以来取代大量的XO(DTO,PO,VO,...)。
- 在横向,已经开发出的工具可以使数据方便地在组件和服务之间传递。
- 系统的配置信息,也是基于名值对的持久数据,当然也可以用数据集以及相关的技术。
- 简单一致的数容器和传输模式,大大简化了组件之间的连通和阻抗匹配问题,开发团队可以集中精力解决领域逻辑问题。
- 服务数据库表和服务本地XML文件的代码几乎是一样的,所以只需截取几条典型记录,存作XML,就可以用于离线测试。
- 因为输入输出都是纯数据,都是同一个类型,Mock Object不再需要,大大简化了单元测试。
- 易于离线开发和离线测试。我做J2EE项目,开发中基本不需要EJB容器。结果我的离线开发工具/环境被其他团队拿去作为正式的离线工具(standalone tool)。离线系统和在线系统具有完全相同的功能,唯一的差别是单线程和多线程。
- 用数据集构造的系统,可以实现简单项目到复杂项目的平滑过渡,或者说复杂项目各阶段之间的平滑过渡。
- 用数据集构造的系统,符合REST和SOA的要求。这里就不展开了。