构建面向对象的应用软件系统框架 第4章

 

第4章          O/R Mapping的一般做法

 

对象和关系型数据库之间的映射,在一个框架中,需要定义映射的规范,在实际开发过程中,对于某个具体的映射,按照规范,使用一定的方法描述映射信息并保存下来,以供程序处理的时候使用。这种描述映射的数据,可以称之为元数据。

什么是元数据?元数据最本质,最抽象的定义为[14]data about data (关于数据的数据)。它是一种广泛存在的现象,在许多领域有其具体的定义和应用。在软件开发领域,元数据被定义为:在程序中不是被加工的对象,而是通过其值的改变来改变程序的行为的数据。它在运行过程中起着以解释方式控制程序行为的作用。在程序的不同位置配置不同值的元数据,就可以得到与原来等价的程序行为。元数据描述数据的结构和意义,就象描述应用程序和进程的结构和意义一样。元数据是抽象概念,具有上下文,在开发环境中有多种用途。

元数据是抽象概念

当人们描述现实世界的现象时,就会产生抽象信息,这些抽象信息便可以看作是元数据。例如,在描述风、雨和阳光这些自然现象时,就需要使用"天气"这类抽象概念。还可以通过定义温度、降水量和湿度等概念对天气作进一步的抽象概括。

在数据设计过程中,也使用抽象术语描述现实世界的各种现象。人们把人物、地点、事物和数字组织或指定为职员、顾客或产品数据。

在软件设计过程中,代表数据或存储数据的应用程序和数据库结构可以概括为开发和设计人员能够理解的元数据分类方案。表或表单由对象派生出来,而对象又由类派生。

在元数据中有多个抽象概念级别。可以描述一个数据实例,然后对该描述本身进行描述,接着再对后一个描述进行描述,这样不断重复,直到达到某个实际限度而无法继续描述为止。通常情况下,软件开发中使用的元数据描述可扩展为二至三级的抽象概念。比如 "loan table" 数据实例可以描述为数据库表名。数据库表又可以描述为数据库表对象。最后,数据库表对象可以用一个抽象类描述,该抽象类确定所有派生对象都必须符合的固定特征集合。

元数据具有上下文

人们通常把数据和元数据的区别称为类型/实例区别。模型设计人员表述的是类型(如各种类或关系),而软件开发人员表述的是实例(如 Table 类或 Table Has Columns 关系)。

实例和类型的区别是上下文相关的。在一个方案中的元数据将在另一个方案中变为数据。例如,在典型的关系型 DBMS 中,系统目录将描述包含数据的表和列。这就意味着系统目录描述数据定义,因而可以认为其中的数据是元数据。但只要使用正确的软件工具,仍然可以象操作其它数据一样对这些元数据进行操作。操作元数据的示例包括:查看数据沿袭或表的版本控制信息,或通过搜索具有货币数据类型的列来识别所有表示财务数据的表。在此方案中,如系统目录这样的标准元数据变为可操作的数据。

元数据有多种用途

可以像使用任何类型的应用程序或数据设计元素一样使用元数据类型和实例信息。将设计信息表达为元数据,特别是标准元数据,可以为再次使用、共享和多工具支持提供更多的可能性。

例如,将数据对象定义为元数据使您得以看到它们是如何构造和进行版本控制的。版本控制支持提供一种查看、衍生或检索任何特定 DTS 包或数据仓库定义的历史版本的方法。开发基于元数据的代码时,可以一次性定义结构,然后重复使用该结构创建可作为特定工具和应用程序的不同版本的多个实例。还可以在现有元数据类型之间创建新关系,以支持新的应用程序设计。

 

从目前的实际应用来看,在已有的一些框架中,通常使用XML文件来保存O/R映射的元数据。例如EJBJDOHibernate等,都无一例外的使用了XML文件,其描述的方法也是大同小异。

下面的例子是一个Entity Bean的部署描述文件的例子,在其中,包含了O/R Mapping的信息。

<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd">

<ejb-jar>

  <description>Here is the description of the test's beans</description>

  <enterprise-beans>

    <entity>

      <description>... Bean example one ...</description>

      <display-name>Bean example two</display-name>

      <ejb-name>ExampleTwo</ejb-name>

      <home>tests.Ex2Home</home>

      <remote>tests.Ex2</remote>

      <ejb-class>tests.Ex2Bean</ejb-class>

      <persistence-type>Container</persistence-type>

      <prim-key-class>tests.Ex2PK</prim-key-class>

      <reentrant>False</reentrant>

      <cmp-version>1.x</cmp-version>

      <cmp-field>

     <field-name>field1</field-name>

      </cmp-field>

      <cmp-field>

     <field-name>field2</field-name>

      </cmp-field>

      <cmp-field>

     <field-name>field3</field-name>

      </cmp-field>

      <primkey-field>field3</primkey-field>

      <env-entry>

     <env-entry-name>name1</env-entry-name>

     <env-entry-type>java.lang.String</env-entry-type>

     <env-entry-value>value1</env-entry-value>

      </env-entry>

    </entity>

  </enterprise-beans>

  <assembly-descriptor>

    <container-transaction>

      <method>

     <ejb-name>ExampleTwo</ejb-name>

     <method-name>*</method-name>

      </method>

      <trans-attribute>Supports</trans-attribute>

    </container-transaction>

  </assembly-descriptor>

</ejb-jar>

 

 

而下面,则是一个JDO的映射文件的例子:

<?xml version="1.0"?>

 

<jdo>

 

    <package name="org.lgd.test">

        <class name="Human" objectid-class="Human$ObjectId">

            <field name="ID" primary-key="true"/>

            <field name="name" primary-key="true"/>

            <field name="dress">

                <collection element-type="Dress"/>

            </field>

        </class>

        <class name="Dress">

            <field name="color">

                <map key-type="String" value-type="ColorTye"/>

            </field>

        </class>

        <class name="ColorTye">

            <field name="red" embedded="true"/>

                     <field name="blue" embedded="true"/>

        </class>

    </package>

 

    <package name="org.lgd.test">

        <class name="Human"/>

        <class name="Asian" persistence-capable-superclass="Human">

            <field name="country">

                <collection element-type="Asua$Chinese"/>

                <extension vendor-name="kodo" key="inverse-owner" value="Human"/>

            </field>

        </class>

        <class name="Human$Asian"/>

    </package>

</jdo>

 

除了使用XML文件来描述元数据信息,现在还有另外一个趋势,就是使用语言本身对元数据的支持来描述映射信息。这个在Microsoft.Net中表现得比较明显。微软提供了一种称为Attribute(特性)的语法来实现语言对元数据的支持(这一点,在JDK1.5中也有了类似的语法,并且EJB3.0就是采用这种描述方式)。

.Net环境下,微软对元数据的描述是:元数据是一种二进制信息,用以对存储在公共语言运行库可移植可执行文件 (PE) 文件或存储在内存中的程序进行描述。我们将代码编译为 PE 文件时,便会将元数据插入到该文件的一部分中,而将代码转换为 Microsoft 中间语言 (MSIL) 并将其插入到该文件的另一部分中。在模块或程序集中定义和引用的每个类型和成员都将在元数据中进行说明。当执行代码时,运行库将元数据加载到内存中,并引用它来发现有关代码的类、成员、继承等信息。

元数据以非特定语言的方式描述在代码中定义的每一类型和成员。元数据存储以下信息:

程序集的说明。

标识(名称、版本、区域性、公钥)。

导出的类型。

该程序集所依赖的其他程序集。

运行所需的安全权限。

类型的说明。

名称、可见性、基类和实现的接口。

成员(方法、字段、属性、事件、嵌套的类型)。

特性。

修饰类型和成员的其他说明性元素。

 

对于一种更简单的编程模型来说,元数据是关键,该模型不再需要接口定义语言 (IDL) 文件、头文件或任何外部组件引用方法。元数据允许 .NET 语言自动以非特定语言的方式对其自身进行描述,而这是开发人员和用户都无法看见的。另外,通过使用属性,可以对元数据进行扩展。元数据具有以下主要优点:

自描述文件。

公共语言运行库模块和程序集是自描述的。模块的元数据包含与另一个模块进行交互所需的全部信息。元数据自动提供 COM IDL 的功能,允许将一个文件同时用于定义和实现。运行库模块和程序集甚至不需要向操作系统注册。结果,运行库使用的说明始终反映编译文件中的实际代码,从而提高应用程序的可靠性。

语言互用性和更简单的基于组件的设计。

元数据提供所有必需的有关已编译代码的信息,以供您从用不同语言编写的 PE 文件中继承类。您可以创建用任何托管语言(任何面向公共语言运行库的语言)编写的任何类的实例,而不用担心显式封送处理或使用自定义的互用代码。

特性。

使用特性,.NET Framework 允许我们在编译文件中声明特定种类的元数据,来批注编程元素,如类型、字段、方法和属性。在整个 .NET Framework 中到处都可以发现特性的存在,特性用于更精确地控制运行时您的程序如何工作。另外,可以通过用户定义的自定义特性向 .NET Framework 文件发出自己的自定义元数据。

为运行库编译代码时,特性代码被转换为 Microsoft 中间语言 (MSIL),并同编译器生成的元数据一起被放到可移植可执行 (PE) 文件的内部。特性使我们得以向元数据中放置额外的描述性信息,并可使用运行库反射服务提取该信息。

.NET Framework 出于多种原因使用特性并通过它们解决若干问题。特性可以描述如何序列化数据,指定用于强制安全性的特性,以及限制实时 (JIT) 编译器的优化以使代码易于调试。特性还可以记录文件名或代码作者,或在窗体开发阶段控制控件和成员的可见性。

可使用特性以几乎所有可能的方式描述代码,并以富有创造性的新方式影响运行库行为。

.Net中,特性是从System.Attribute 派生的特殊的类的。

下面的例子是Websharp框架的一个使用Attribute来描述O/R Mapping的例子。

     [TableMap("Schdule","GUID")]    

     [WebsharpEntityInclude(typeof(Schdule))]

     public abstract class Schdule : PersistenceCapable

     {

         [ColumnMap("GUID",DbType.String,"")]

         public abstract string GUID{get;set;}    

        

         [ColumnMap("UserID",DbType.String,"")]

         public abstract string UserID{get;set;}       

        

         [ColumnMap("StartTime",DbType.DateTime)]

         public abstract DateTime StartTime{get;set;}      

        

         [ColumnMap("EndTime",DbType.DateTime)]

         public abstract DateTime EndTime{get;set;}        

        

         [ColumnMap("Title",DbType.String,"")]

         public abstract string Title{get;set;}        

        

         [ColumnMap("Description",DbType.String,"")]

         public abstract string Description{get;set;}      

        

         [ColumnMap("RemidTime",DbType.DateTime)]

         public abstract DateTime RemidTime{get;set;}      

        

         [ColumnMap("AddTime",DbType.DateTime)]

         public abstract DateTime AddTime{get;set;}        

        

         [ColumnMap("Status",DbType.Int16,0)]

         public abstract short Status{get;set;}        

     }       

 

按照前面所讨论的内容,在一个应用系统中,如果我们能够确定数据的表现形式,并且能够有一个规则的对象/关系型映射方式,那么,我们就可以设计一个框架,来完成对象的操纵,完成对象和关系型数据之间的转换,用更加简单的话来说,就是完成数据的增、删、改和查询的操作。

管理对象和关系型数据之间的转换,是O/R Mapping框架的基本功能,在一些框架中,通常还会包含一些其他方面的功能,用以获取更好的性能或者更高的可靠性,一些比较高级的框架则以中间件的形式存在,例如一些EJB容器。这些常见的功能包括:

Ø         缓存

Ø         事务处理

Ø         数据库连接池

等等。

对象存储的一般过程是:

Ø         程序提交PO保存

Ø         状态管理器读取PO的状态,确定需要保存的内容

Ø         读取元数据信息,和数据库信息,生成相应的SQL语句

Ø         获取数据库连接

Ø         存储数据

Ø         修改PO的状态

为了方便框架对对象的操作,通常都需要实体类的编写符合某种规范。然后,可以使用统一的方法来操纵对象。

在数据操纵的设计方面,有两种方法倾向,一种是把对数据的增、删、改的方法和实体类设计在一起,另一种方法是把实体类和对实体类的操作分开,设计一个通用的接口来实现对对象的操纵。

第一种方法的典型是EJB。在EJB中,O/R Mapping通过Entity Bean来完成,在Entity Bean中,实体数据和对实体数据进行操纵的方法是在一起的。我们可以看一下EJB中关于Entity Bean的规范定义。

    首先,一个Entity Bean必须定义Home接口,这个接口扩展EJBHome接口,并且在这个接口中定义crreatefindByrimaryKey等方法,如,要定义一个Customer类的Home接口,可以如下来定义:

   

public interface CustomerHome extends EJBHome

{

     public Customer create(String CustomerID,String CustomerName)

          throws RemoteException , CreateException;

     public Customer findByrimaryKey(String CustomerID)

          throws RemoteException , FinderException;

}

其次,定义远程访问接口,这个接口扩展EJBObject,在这里,可以定义一些业务方法,例如,一个Customer实体类的远程访问接口可以如下定义:

public interface Customer extends EJBObject

{

     public void setCustomerName(String customerName) throws RemoteException;

     public String getCustomerName() throws RemoteException;

}

最后,需要定义Bean的实现类,这个类实现EntityBean接口,上述的Customer实现类,可以定义如下:  

public class CustomerEJB implements EntityBean

{

     public String customerID;

     public String customerName;

     public void setCustomerName(String customerName)

     {

          this.customerName = customerName ;

     }   

     public String getCustomerName()

     {

          return this.customerName;

     }

}

Bean实现中,有一些相关的方法,是用来描述是实体类如何同数据库进行交互的,这些方法包括ejbCrrateejbLoadejbStoreejbRemove等,关于细节部分的内容,读者可以参考EJB的相关资料。从上面可以看出,在EJB中,对于实体类来说,实体类和对实体类的操作,都是定义在一起的,当然,对于CMP来说,具体实体类是如何同数据库打交道的细节,是由容器来管理的。

 

另外一种方式,是将实体数据同对对象的操作分开。这种方式,一般都会定义一个访问接口来供调用。例如,我们可以考察一下JDO的方法。

JDO中,任何实体类都必须实现PersistenceCapable接口,当然,这个接口的实现,可以通过手工的编码的方式来实现,但是在更多的情况下,程序员只需要编写一个普通的Java类,然后,通过代码增强器来实现。

在数据操纵方面,主要的访问接口是PersistenceManager接口。所有对对象的保存、删除的操作都可以通过这个接口来进行。在对象查询方面,JDO定义了Query接口,可以从这个接口,根据条件查询数据库,并返回对象的集合。

下面的例子演示了一个简单的网数据库中保存一个Customer对象的过程:

Customer customer = new Customer(“12345”);

customer.setCustomerName(“My Customer”);

PersistenceManagerFactory pmf = JDOHelper.getPersistenceManagerFactory(props);

PersistenceManager pm = pmf.getPersistenceManager();

pm.makePersistent(customer);

pm.close();

 

比较上述两种方法,应该说各有特点,但是,现在比较倾向的,是把实体数据同对对象的操作分开的方式,主要理由在于:

1、                把实体数据同对对象的操作分开,可以得到一个统一的操作接口,而不是在所有的实体对象中分别来实现,系统的结构更加清晰。

2、                更加重要的是,在分布式的N层应用系统结构中,对对象的逻辑处理存在于应用服务器上,而应用服务层和表现层之间只传递数据。如果采用两者合一的方式,那么想象一下,在表现层调用Customer对象的Add方法的时候,会出现什么样的情况?

 

在组合一个业务逻辑的时候,我们通常的工作是处理一组对象,进行某些运算,然后,将结果显示或者保存起来。在这种情况下,我们通常所处理的对象是个别的。在新增、修改和删除对象的时候,通常都是一个个操作的。

数据保存,最终是要供检索的,有的时候,我们需要查询一组对象,有时候还会使用多个组合查询条件来检索对象。关系型数据库成功的一个重要方面,是得益于SQL查询功能的强大,使用SQL语句,可以很方便的操纵数据库。在O/R Mapping框架中,提供一个相当的查询机制,是非常有必要的。因为对象查询的复杂性,一般会提供单独的机制来实现对象的查询。

我们可以来考察一下JDO。在JDO中,提供了Query接口,以及JDOQL语法,用来提供对象的查询功能。

下面是Query接口的定义,供参考:

public interface Query extends java.io.Serializable

{

     void setClass(Class cls);

    void setCandidates(Extent pcs);

    void setCandidates(Collection pcs);

    void setFilter(String filter);

    void declareImports(String imports);

    void declareParameters(String parameters);

    void declareVariables(String variables);

    void setOrdering(String ordering);

    void setIgnoreCache(boolean ignoreCache);  

    boolean getIgnoreCache();

    void compile();

    Object execute();

    Object execute(Object p1);

    Object execute(Object p1, Object p2);

    Object execute(Object p1, Object p2, Object p3);

    Object executeWithMap (Map parameters);

    Object executeWithArray (Object[] parameters);

    void close (Object queryResult);

    void closeAll ();

}

下面的代码,是一个使用JDOQuery接口进行查询的例子:

Class empClass = Employee.class;

Extent clnEmployee = pm.getExtent (empClass, false);

String filter = “salary > sal”;

Query q = pm.newQuery (clnEmployee, filter);

String param = “Float sal”;

q.declareParameters (param);

Collection emps = (Collection) q.execute (new Float (30000.));

 

 

posted on 2005-11-29 14:26  孙策  阅读(825)  评论(0编辑  收藏  举报