关于元数据
系统开发中存在各种各样的数据,比如Tom是一个年龄为30岁的男性员工、Liliy是一个21岁的女性员工、这张报表是今年第三季度的利润表、那张报表是今年上半年的销售波动图、对话框上有三个按钮控件、窗口上有一个多行文本控件和一个保存按钮、这个WebService提供了股票实时情况查询的服务、那个WebService提供了查询天气预报的服务。
以上数据存在很多共性的特征,这些特性都可以通过某种形式进行抽象。
对于“Tom是一个年龄为30岁的男性员工”、 “Liliy是一个21岁的女性员工”,在数据库级别就会抽象成含有FIdVarchar(50)、FNameVarchar(50)、FAge(int)、FSex(int)四个字段的数据库表T_Employee,在Hibernate中就被抽象成含有id、name、age、sex四个字段的JavaBean以及对应的hbm配置文件。
这些数据是平台无关的,在描述“Tom是一个年龄为30岁的男性员工”这条数据的时候,它即可以是保存在数据库中的,也可以是保存在XML配置文件中的,甚至有可能只是写在一张便条上的。与此相反的是,对这些数据的抽象方式大都是与特定平台相关的,是无法移植的。比如要把数据的存储方式由数据库改为XML文档,那么就必须针对XML文件的存取特点重新进行抽象。由于抽象方式是平台相关的,这些抽象出来的模型就不具有通用性,无法通过统一的方式来读取它们。比如要读懂T_Employee这张表中的字段的含义就要去查阅数据字典,要读懂便条上的“Tom 30 m”就要去询问写便条的人。
元数据(MetaData)是MDA中非常重要的概念。它通过统一的、平台无关的、规范的方式对数据的模式特征进行描述,通过一个模型结构来表达通用的信息,它集设计模型、开发模型与运行模型为一体。元数据具有如下几个作用。
(1)元数据是独立于平台的,无论使用什么技术平台,元数据本身是不受影响的,这保证了先期工作成果的效用最大化。
(2)元数据是生成平台相关模型的基础,可以使用代码生成器等工具将元数据转换成平台相关代码。
(3)元数据为运行时系统提供了统一的可读的系统模型,系统运行时可以使得实体对象通过运行时元数据模型来得知自身的结构、自身的特征、在系统模型中的位置以及与其他对象之间的关系等。这样就可以从一个新的角度来观察、设计、开发系统。
(4)元数据模型是系统运行不可或缺的部分,如果直接修改平台相关代码而不修改元数据,就会造成系统运行异常,这就强迫保证元数据模型与代码同步,保证了设计模型和实现代码的一致性。
(5)元数据本身就是一个设计模型。系统设计人员可以使用元数据进行系统建模,在某种程度上元数据可以取代UML图等传统的设计模型。设计人员将设计完成的元数据模型交给开发人员,开发人员使用代码生成器将元数据转换成平台相关代码,然后就可以基于这些平台相关代码进行开发了。元数据起到了设计人员和开发人员沟通桥梁的作用,设计人员的工作立即就可以转换为可以运行的平台相关代码。
7.2.1 元数据示例
枚举类型在不同的系统中有不同的表示方式,而且有不同的模型描述方式(即枚举有哪些项、项的值是多少等信息),有的平台还没有提供足够的模型描述方式。客户类型包括:普通客户、会员客户、VIP客户。
在JDK 1.5中可以表示为enumCustomerTypeEnum{Normal, Member,VIP},取得CustomerTypeEnum枚举类型中定义的所有枚举项的方法为CustomerTypeEnum.values(),取得“Normal”这个字符串对应的枚举项的方法为Enum.valueOf(CustomerTypeEnum.class,"Normal")。
在JDK 1.4中使用ApacheCommons包提供的Enum类可以表示为:
public class CustomerTypeEnum extendsorg.apache.commons.lang.enums.Enum
{
publicstatic DataTypeEnum Normal= new DataTypeEnum("Normal");
publicstatic DataTypeEnum Member= new DataTypeEnum("Member");
publicstatic DataTypeEnum VIP= new DataTypeEnum("VIP");
privateDataTypeEnum(String name)
{
super(name);
}
}
取得 CustomerTypeEnum枚举类型中定义的所有枚举项的方法为EnumUtils.get-EnumList(CustomerTypeEnum.class),取得“Normal”这个字符串对应的枚举项的方法为EnumUtils.getEnum(CustomerTypeEnum.class, "Normal")。
在C# 中,可以表示为enum CustomerTypeEnum{Normal, Member,VIP},取得Customer-TypeEnum枚举类型中定义的所有枚举项的方法为Enum.GetNames(typeof(CustomerTypeEnum)),取得“Normal”这个字符串对应的枚举项的方法为Enum.Parse(typeof(CustomerTypeEnum),"Normal")。
在 Delphi中,可以表示为type CustomerTypeEnum=(Normal, Member,VIP);没有提供取得CustomerTypeEnum枚举类型中定义的所有枚举项的方法,取得“Normal”这个字符串对应的枚举项的方法也没有直接提供,必须借助RTTI。
要将一个平台上的CustomerTypeEnum移植到另一个平台,必须用目标平台的枚举语法重新改写,而且使用的取得枚举类描述信息的方式也要发生变化,这都给系统的移植带来了很大的工作量。
【例7.1】元数据示例。
为了解决这个问题,我们设计一个元数据模型:
<Enum>
<Name>CustomerTypeEnum</Name>
<Items>
<Item name="Normal"displayName="普通客户"></Item>
<Item name="Member"displayName="会员客户"></Item>
<Item name="VIP"displayName="VIP客户"></Item>
</Items>
</Enum>
提供一个描述这个元数据模型的描述类:
//枚举描述类
public class EnumInfo
{
…
//得到所有的枚举项
publicEnumItemInfo[] getEnumItems();
//得到名字为name的枚举项的信息
publicEnumItemInfo getEnumItem(Stringname);
}
//枚举项描述类
public class EnumItemInfo()
{
…
//枚举项的名字
publicString getName();
//枚举项的显示信息
publicStringgetDisplayName();
}
提供一个读取元数据模型的API:
public class EnumMetaDataLoader
{
…
//加载元数据类型enumTypeName对应的元数据模型
publicEnumInfo loadEnum(String enumTypeName)
{
…
}
}
枚举元数据模型的描述类和读取元数据模型的API的实现代码仍然是平台相关的,因为这些类都是要被特定平台使用的。因为XML解析在各个平台是大同小异的,所以这些描述类和API的实现方式的移植是非常简单的。
使用这样的元数据模型我们还可以定义其他的枚举类型,比如:
<Enum>
<Name>SexEnum</Name>
<Items>
<Item name="Male"displayName="男"></Item>
<Item name="Female"displayName="女"></Item>
</Items>
</Enum>
在JDK1.4平台下,使用代码生成器将SexEnum的元数据模型转换成JDK 1.4下的枚举代码:
public class SexEnum extendsorg.apache.commons.lang.enums.Enum
{
publicstatic SexEnum Male= new DataTypeEnum("Male");
publicstatic SexEnum Female= new DataTypeEnum("Female");
privateSexEnum String name)
{
super(name);
}
}
当要得到所有SexEnum定义的枚举项的时候,按如下方式调用:
EnumInfo enumInfo =EnumMetaDataLoader.getInstance().loadEnum("SexEnum");
EnumItemInfo[] itemInfos =enumInfo.getgetEnumItems();
for(inti=0,n=itemInfos.length;i<n;i++)
{
EnumItemInfoitemInfo = itemInfos[i];
System.out.println("项名称:"+itemInfo.getName()
+";显示名称:"+getDisplayName());
}
这样我们不用再去调用特定平台的API实现了,元数据信息提供了比平台API更多的功能,并且写出的代码不会受平台API的限制。
若某天客户提出要增加一种“不详”的性别类别,如果 开发人员直接修改生成的SexEnum类,在其中加入“不详”的性别类别的枚举定义的话,系统就会工作不正常,因为没有修改SexEnum元数据。这样就限制了开发人员直接修改SexEnum 类,这样开发人员只能去修改SexEnum元数据,然后用代码生成器来重新生成SexEnum类代码。这规范了开发人员的行为,保证了设计模型与实现代码的一致性。
若某一天,公司决定将开发平台从Java迁移到C#,那么对于枚举这部分需要做的改造工作就是用C#重写元数据模型描述类和元数据读取API,并开发一个针对C#枚举的元数据转换器,系统所有的枚举就都可以自动转换成C#下的了。这保证了前期对于枚举元数据模型的设计开发成本利用的最大化。
由于枚举在各个平台之间差异并不算大,而且一个平台整体从Java迁移到C#的可能性也非常小,所以元数据在这里起到的作用并不大。但是在Java平台上ORM工具的迁移倒是很有可能的,要想体会元数据的更重要的作用就要看案例系统的实体元数据了。
7.2.2 元元数据
元数据是对数据共性的抽象,而不同的元数据本身也是具有共性的,以上一节的两个枚举元数据来说,客户类型枚举元数据与性别枚举元数据为共同的模式:
<Enum>
<Name>枚举名称</Name>
<Items>
<Item name="名称"displayName="显示名称"></Item>
…
</Items>
</Enum>
可以定义一种模型来描述所有枚举元数据的共性特征,也就是枚举元数据的元数据(Metadata ofmetadata)。这种对元数据进行抽象描述的形式被称为元元数据(MetaMetaData)。
7.2.3 设计时与运行时
元数据的直接表示形式被称为设计时元数据,而在运行的时候能被系统读取的形式(比如上边的EnumInfo)被称为运行时元数据。通常,运行时元数据描述的特性是设计时元数据的特性的子集。
系统承担着设计模型与运行时模型的多重责任,而且元数据还作为代码生成器的“源”,承载着描述目标代码的作用。这些责任之间有相交的部分,也有自己独特的部分。举例来说,一个描述实体对象的元数据,它描述这个实体对象有哪些字段、字段的类型是什么、和其他实体对象之间有什么关系等信息,而作为代码生成器的“源”,它还要描述一些目标平台特有的东西,比如当目标平台为Hibernate的时候,就需要指定主键字段的生成策略、关联字段的LazyLoad策略、Casade策略等。从严格意义上来讲,为了维持元数据的平台无关性,这些平台相关的特性是不能放在元数据中的,而应该放在一个描述平台相关属性的地方,不过这样就使得元数据模型过于复杂。一个较好的策略是在元数据中增加一个专门存放这些平台相关属性的区域。
运行时的元数据是要被平台相关代码访问的,如果运行时元数据中包含平台相关特性的话,就会导致以后平台移植难度加大,而且也混淆了设计时语义与运行时语义之间的界限。所以运行时的元数据中一定不能包含平台相关特性。
7.2.4 元数据设计的基本原则
除了上边提到的运行时的元数据中一定不能包含平台相关特性之外,在元数据的设计中,“适可而止”也是需要铭记在心的核心原则。对元数据描述的范围要适可而止,不要试图包罗万象。运行时元数据是能够给运行时的系统提供元数据的信息的,这在一定程度上简化了系统的开发,但是切不可把应该写在代码中或者写到配置文件中的信息写到元数据中。比如在实体对象元数据中,给字段增加了“allowNull”特性来表示此字段是否允许为空。系统保存实体对象的时候,可以读取此实体对象对应的元数据,进而取得所有字段的是否为空的特性,从而对数据进行校验。这是对运行时元数据非常合理的运用。但是如果试图把字段为空时提示什么样的信息、字段最大长度是多少、字段是否进行加密操作等特性加入元数据的话就会使得元数据模型过于庞大,这也违反了“适可而止”这一基本原则。如果元数据直接驱动系统的运行过程,并且有取代程序代码的趋势的话,就说明设计人员对元数据概念理解错误了,用元数据驱动系统运行虽然减少了代码的编写,但是这些本不应该放在元数据中的特性是不完备的,一旦需要扩展就会遇到难以逾越的鸿沟。
由于客户需求的复杂性,模型结构不能表达出所有业务的处理过程,仍然存在需要利用编程语言才能完成的业务功能。元数据模型解决大多数通用的问题,而对于具有差异性的问题还是要通过编码来完成的,不应该让运行时元数据承担过多的运行时语义。
7.2.5 此“元数据”非彼“元数据”
元数据这个词汇并不是MDA发明的,在其他领域“元数据”早已经被使用了,在软件开发领域,“元数据”也不是MDA中才有的。
JDK5.0的annotation机制也被称为元数据,它为属性的物理驻留位置提供了新的选择。annotation使得代码具有自解释的能力,代码变成能同时提供行为及自我描述能力的实体,也就是说代码从一维变成二维的了。使用JDK提供的API就可以从代码中读取到这些描述信息。
public String getName(){…}
这段代码中类似JavaDoc的东西就是annotation,它描述了name这个属性对应着数据库中的类型为“Varchar(44)”名称为“FName”的字段。Hibernate3、EJB3都推荐并且支持这种方式。
在Hibernate本身也有元数据机制,在Hibernate的包org.hibernate.metadata中的类就是提供元数据支持的API,通过它们能读取到一个实体对象有哪些字段、字段的类型是什么、是否允许为空、是否关联其他的实体对象等。
JDK的annotation、Hibernate中的元数据都符合元数据的定义,它们也是真正的元数据。它们与MDA中的元数据的最主要区别就是是否具有平台无关性。很显然JDK的annotation、Hibernate中的元数据都不能脱离它们所依赖的平台,元数据中有很多描述平台专有属性的东西,无法作为一个跨平台的元数据引擎使用。不过这些元数据能反应平台的更多的细节,如果合理利用将极大地提高开发效率。在后边关于HibernateDTOGenerator的分析中读者将会看到我们是如何使用Hibernate的元数据来实现DTO产生器的。