摘要:

本文推荐的一个叫Amber的框架提供了一种相反的轻量级实现。这种实现利用Java注解(annotations)来管理JavaBeans的CRUD周期(Create Read Update Delete)。事务处理被交还给数据库,而XML映射描述符则被注解代替。本文所面向的读者是那些对不使用XML描述符来有效操纵数据库感兴趣的Java中级开发者。

计算机业中有一条不成文的说法:面向对象软件和关系数据库之间的数据共享,最好通过对象/关系(O/R)映射框架来进行,而这种框架是实体关系(ER)模型依赖于面向对象模型的。本文推荐的一个叫Amber的框架提供了一种相反的轻量级实现。这种实现利用Java注解(annotations)来管理JavaBeans的CRUD周期(Create Read Update Delete)。事务处理被交还给数据库,而XML映射描述符则被注解代替。本文所面向的读者是那些对不使用XML描述符来有效操纵数据库感兴趣的Java中级开发者。


动机

普通O/R映射框架是非常强大的;但是在介绍如何设计和部署时,一些问题却很少被提及。我们将列出这些缺点,并针对这些问题来演示一个叫Amber的框架。
1.        OO驱动的数据模型导致了过于简单的ER模型。
2.        XML描述符使得维护困难。
3.        在O/R层进行事务处理非常困难。
4.        现有框架的学习曲线相对陡峭。

在两种模型之间交换数据,比如ER模型和OO模型,必须克服所谓的阻抗不匹配。对于大多数O/R模型工具来说,对象模型处于支配地位。大体上,这意味着Java持久层负责从现有的对象模型生成ER模型。这个主意非常引人注目,因为当商务模型确定以后,开发团队就再也不需要担心持久化的问题了。

对于常规的O/R工具而言,ER模型是一个结果,一个产物,顶多是一个容器。而商务过程实际上是按ER模型设计的,这就导致了两者之间的不协调。这样的话,ER模型的调整就非常困难,甚至是不可能的,因为O/R框架可能会在任何时候重构ER模型。同样,当商务过程发生改变时,O/R域的调整会自动重构ER域,于是ER模型变得令人费解,并且有时性能会下降到临界点。

还有另一个问题。会被持久化的类需要在外部XML描述(映射)文件中部署。初看不错,但是当我们处理现存的系统时,这很快就成了烦人的事。只要发生了一点变动,就得有不只一个地方需要修改,也就是源代码和映射文件。

最后,现有的O/R框架是为了处理事务而设计的。综合来看,这不是必须的,因为存储容器(例如关系数据库)是非常傻的容器。尽管我们不得不进行事务处理,但那并不是我们想要的。这些应该是数据库的事。

介绍Amber

Amber从相反的角度来解决数据交换的问题。它采用ER模型为参考来确立OO结构。它还采用存储过程作为数据库访问主要方式,存储过程提供了访问数据库的唯一途径,并且完全的建立起事务处理机制。最后中间层会被实现为一系列存储过程的集合。这意味着ER模型的专家,数据库管理员要负责设计和优化包括存储过程在内的一系列问题,于是比起自动创建的系统,新的系统能够拥有更好的结构,更快的速度和更安全的访问。因此,许多难题迎刃而解。
•        事务能够(或者说应该)被封装进存储过程。
•        读操作仅返回结果集合。
•        写操作只需要调用存储过程,而不是在Java代码中嵌入SQL。
•        使用存储过程,就不会因为SQL注入而导致安全漏洞。

当然,这意味着通常在Java代码中处理的事被转移到存储过程中了。这样不会有人犯错了。这对Java开发者来说有莫大的好处。

映射

Amber的核心在于,不管被提交到数据库的查询是什么,查询结果都是一列Java对象。当然,这是从Java开发者的角度来看的。那么剩下的问题只是把字段映射到对象的属性。以及在把数据写入数据库时,把Java对象的属性映射到存储过程的参数。

Amber把结果集映射到JavaBean,并用相同的机制在增删改时把bean的内容映射到参数。对于JavaBean的相关信息和定义,请查看资源那一段。

这种做法用到了Java语言的新特性,这个叫做注解的特性是从J2SE 5.0开始使用的。

注解,在JSR 175中也叫做“元数据”,是一种辅助代码,可以用来提供类,方法,属性的详细信息。在Javadoc API中,元数据本来是为了用来内联文档的。所以,在不干扰正常代码的前提下,注解可以用来描述代码的具体作用。如果你想知道关于注解更多的信息以及作用,请参考Tiger: A Developer's Notebook,或者看看我写的一篇更有趣的文章" Annotations to the Rescue"。

一步一步来

我们来解决一个小的持久化问题。从数据库读取一列Jedi对象,我们假设返回的结果集看起来像下面的表格。请注意,我们接下来的讨论并不依赖于这些表格,尽管这些例子实际上都是基于这些表格的。一般来说,我们得到的表列数据是通过使用少量的SQL连接多个表或者视图来得到的。(当然,先向星球大战的爱好者们告罪。)
image

我们先定义一个叫Jedi的简单类。
public class Jedi {

   private Integer _id;
   private String _name;
   private Double _forceRating;
   private Integer _age;
   private Boolean _alive;

   @ColumnAssociation(name="jedi_id")
   public void setId( Integer id ) {
      _id = id;
   }
   @ColumnAssociation(name="name")
   public void setName( String name ) {
      _name = name;
   }
   @ColumnAssociation(name="force_rating")
   public void setForceRating( Double fr ) {
      _forceRating = fr;
   }
   @ColumnAssociation(name="age")
   public void setAge( Integer age ) {
      _age = age;
   }
   @ColumnAssociation(name="alive")
   public void setAlive( Boolean alive ) {
      _alive = alive;
   }

   @ParameterAssociation(name="@jedi_id",
   index=0, isAutoIdentity=true)
   public Integer getId() {
      return _id;
   }
   @ParameterAssociation(name="@name", index=1)
   public String getName() {
      return _name;
   }
   @ParameterAssociation(name="@force_rating",
   index=2)
   public Double getForceRating() {
      return _forceRating;
   }
   @ParameterAssociation(name="@age", index=3)
   public Integer getAge() {
      return _age;
   }
   @ParameterAssociation(name="@alive", index=4)
   public Boolean getAlive() {
      return _alive;
   }
}


这里发生了什么?你在类中看到getter和setter方法上面有两种注解。

注解@ColumnAssociation用来把JavaBean中的setter方法和结果集中的一个字段连接起来,这样从数据库中得到的表列数据就能够被Amber 写入bean的属性里。注解@ColumnAssociation只适用于setter方法,因为Amber使用这些注解以及从数据库中读取的相应值来寻找和调用那些方法。

同样,getter方法需要@ParameterAssociation注解来把JavaBean的属性和调用增删改操作时的参数连接起来。这个注解只适用于getter方法,因为Amber使用getter方法来把值填到参数里。因为JDBC的缘故,需要提供参数的索引。这个也许的多余的,取决于数据库以及存储过程是否需要,不过为了完整性,以及遵循JDBC API规范,最好还是提供一下。

必须提供一个无参数的构造函数,因为这个类会被自动构造(通过反射)。在上面的类中,没有无参数构造函数是允许的,因为我们没有提供其他的构造函数,但是当我们增加了额外的构造函数时,就必须提供一个明确的给Amber。

这个JavaBean的作用是从数据库里读出数据以及写回数据库。完全不需要外部的描述文件。注意,我们也可以用这种方式建立任何类,不只是JavaBean。

你也许会奇怪:为什么用注解?为什么不是像JavaBean那样通过属性名来隐式关联?我们这么做,是为了让我们的设计保持一定的自由度。换句话说,我们的Java代码不需要依赖于ER模型设计的字段名。如果你已经习惯于操作表,你也许不同意这点,但是当你使用的存储过程里需要连接表和视图时,你就不得不使用别名。

Amber的连接器和JDBC

在我们开始读写数据库之前,我们需要与数据库建立连接。Amber使用一个Connector来访问数据库。简单说来,这就是把数据库驱动和连接字符串结合使用而已。在应用中,我们使用一个ConnectorFactory来管理可用连接。像下面的代码那样,我们使用一个本地的type-4驱动来初始化一个到SQL server的连接。我们假设服务器的名字是localhost,数据库的名字是jedi,用户名是use,密码是theforce,为了简单一点,我在下面的代码中省略了全部的异常处理。
String driverClassName = 
   "com.microsoft.jdbc.sqlserver.SQLServerDriver";
String url =
   "jdbc:microsoft:sqlserver://" +
   "localhost;databasename=jedi;" +
   "user=use;pwd=theforce";
Amber's Connector is associated with a String, alias under which it remains accessible from the ConnectorFactory. Here, we're going to use the alias starwars.
Amber的Connector使用一个String作为别名来从ConnectorFactory获取连接,接下来,我们将使用别名starwars。
ConnectorFactory.getInstance().add(
   "starwars", driverClassName, url
);


因为Connector是对JDBC连接的轻量级封装,所以我们可以像以前一样操作这个连接。

读取

封装在Connector外面的是一个BeanReader对象,它需要一个Connector和一个Class来告诉reader从数据库读出的bean是什么类型的。现在读取一列Jedi对象就只需要下面几行了。
Connector connector = 
   ConnectorFactory.createConnector( "starwars" );
BeanReader reader =
   new BeanReader( Jedi.class, connector );
Collection<Jedi> jediList =
   reader.executeCreateBeanList(
      "select * from jedi"
   );


这段代码使用了一种叫泛型的新特性,这种特性是从J2SE 5.0开始使用的。Collection声明的那行代码表明jediList一律由Jedi类型的对象组成。编译器在这里会发出警告,reader只有在运行时刻才知道会产生什么类型的对象。因为在J2SE 5.0中,泛型在执行的时候会把类型信息抹掉,所以可能导致不安全的类型转换。非常遗憾的,因为同一原因,我们不能把BeanReader写成BeanReader<Jedi>。简单说,就是Java的反射和泛型不能混合使用。

那么复合结构会如何呢?好吧,我们有几种方法可以处理这个问题。比如,我们在Jedi和Fighter (例如,每个Jedi有好几艘太空战斗机)之间有一个一对多的关系。在数据库中,Fighter的数据看起来像下面那样。
image

换句话说,Luke有两艘战斗机(X-和B-Wing),Yoda则拥有一艘Star Destroyer,而Obi Wan已经死掉了。

数据之间的关系在OO域中有几种方法可以模型化。我们只挑选最简单的那种。所以我们需要Jedi类可以拥有一组Fighter对象作为成员。下面的Fighter类是为了让Amber使用而建立的。
public class Fighter {

   private Integer _id;
   private Integer _jediId;
   private String _name;
   private Double _firepowerRating;
   private Boolean _turboLaserEquipped;

   @ColumnAssociation(name="fighter_id")
   public void setId( Integer id ) {
      _id = id;
   }
   @ColumnAssociation(name="jedi_id")
   public void setJediId( Integer jediId ) {
      _jediId = jediId;
   }
   @ColumnAssociation(name="name")
   public void setName( String name ) {
      _name = name;
   }
   @ColumnAssociation(name="firepower_rating")
   public void setFirepowerRating( Double firepowerRating ) {
      _firepowerRating = firepowerRating;
   }
   @ColumnAssociation(name="turbo_laser_equipped")
   public void setTurboLaserEquipped(
      Boolean turboLaserEquipped ) {
      _turboLaserEquipped = turboLaserEquipped;
   }
   @ParameterAssociation(name="@fighter_id",
      index=0,isAutoIdentity=true)
   public Integer getId() {
      return _id;
   }
   @ParameterAssociation(name="@jedi_id",index=1)
   public Integer getJediId() {
      return _jediId;
   }
   @ParameterAssociation(name="@name",index=2)
   public String getName() {
      return _name;
   }
   @ParameterAssociation(name="@firepower_rating",
      index=3)
   public Double getFirepowerRating() {
      return _firepowerRating;
   }
   @ParameterAssociation(name="@turbo_laser_equipped",
      index=4)
   public Boolean getTurboLaserEquipped() {
      return _turboLaserEquipped;
   }
}


下面的是改进后的Jedi类。它新增加了一个List<Fighter>类型的成员。下面的J2SE 5.0代码表明链表只包含Fighter类型的对象。新增加的代码用粗体表示。
public class Jedi {

   private Integer _id;
   private String _name;
   private Double _forceRating;
   private Integer _age;
   private Boolean _alive;
  
   private ArrayList<Fighter> _fighterList =
      new ArrayList<Fighter>();
  
   @ColumnAssociation(name="jedi_id")
   public void setId( Integer id ) {
      _id = id;
   }
   @ColumnAssociation(name="name")
   public void setName( String name ) {
      _name = name;
   }
   @ColumnAssociation(name="force_rating")
   public void setForceRating( Double forceRating ) {
      _forceRating = forceRating;
   }
   @ColumnAssociation(name="age")
   public void setAge( Integer age ) {
      _age = age;
   }
   @ColumnAssociation(name="alive")
   public void setAlive( Boolean alive ) {
      _alive = alive;
   }

   @ParameterAssociation(name="@jedi_id",
      index=0, isAutoIdentity=true)
   public Integer getId() {
      return _id;
   }
   @ParameterAssociation(name="@name", index=1)
   public String getName() {
      return _name;
   }
   @ParameterAssociation(name="@force_rating",
      index=2)
   public Double getForceRating() {
      return _forceRating;
   }
   @ParameterAssociation(name="@age", index=3)
   public Integer getAge() {
      return _age;
   }
   @ParameterAssociation(name="@alive", index=4)
   public Boolean getAlive() {
      return _alive;
   }
   public ArrayList<Fighter> getFighterList() {
      return _fighterList;
   }
   public void setFighterList( ArrayList<Fighter> fighterList ) {
      _fighterList = fighterList;
   }
}


从数据库读取Jedis的代码看起来像下面这样:
Connector connector = 
   ConnectorFactory.getInstance().createConnector( "starwars" );
BeanReader jediReader =
   new BeanReader( Jedi.class, connector );
BeanReader fighterReader =
   new BeanReader( Fighter.class, connector );
Collection<Jedi> jediList =
   reader.executeCreateBeanList( "select * from jedi" );
for( Jedi jedi : jediList ) {
   String query =
      "select * from fighter where jedi_id = " + jedi.getId();
   Collection<Fighter> fighters =
      fighterReader.executeCreateBeanList( query );
   jedi.setFighterList(
      new ArrayList<Fighter>( fighters ) );
}


瞧,这就是Jedi们拥有的战斗机了。请注意,我们并没有敲出把Fighter读进Jedi的代码。因为Jedi和Fighter会严格的匹配。你会说上面的代码在依赖注入模式中只是一些部件。也许我是在说大话,我只想说:把互相依赖的东西分开,并且使分布在各处的代码共同工作。如果你想在这方面知道得更多,请看Martin Fowler的"Inversion of Control Containers and the Dependency Injection pattern"。

写入

现在,该写入了。把改变了的Jedi写入数据库只需要下面几行代码。
Connector connector = 
   ConnectorFactory.getInstance().createConnector( "starwars" );
BeanWriter writer =
   new BeanWriter( Jedi.class, connector );
writer.executeStringUpdate(
   sampleBean, "UpdateJedi" );


这里,数据库访问通过生成SQL查询字符串。最下面一行代码生成执行字符串并发送到数据库,修改了使用1000作为id的Jedi(就是Obi Wan)的状态(假设我们把属性alive改为true,把forceRating改为6.0)。
UpdateJedi 
   @name='Obi Wan Kenobi', @jedi_id=1000,
   @alive=1, @force_rating=6.0, @age=30



如果你想建立一个新的Jedi,我们只需要简单的构造一个新的Jedi并用下面的代码写入数据库。
Jedi newJedi = new Jedi();
newJedi.setName( "Mace Windu");
newJedi.setAge( 40 );
newJedi.setAlive( false );
newJedi.setForceRating( 9.7 );
Connector connector =
    ConnectorFactory.getInstance().createConnector( "starwars" );
BeanWriter writer =
    new BeanWriter( Jedi.class, connector );
writer.executeStringInsert(
    newJedi, "InsertJedi" );


你会注意到,我们使用了不同的方法和存储过程来写入数据。最后字符串会是这样。
InsertJedi 
   @name='Mace Windu', @alive=0,
   @force_rating=9.7, @age=40


发生了什么?我们假设属性jediId是由数据库自动生成的。实际上,在上面定义的Jedi类中,我们指定@ParameterAssociation的属性isAutoIdentity=true来达成这一点。因为数据库会给bean提供主键,所以参数@jedi_id就省略了。

这里需要注意一下。因为jediId是由数据库提供的,所以这个数据一定会通过存储过程InsertJedi传回数据库。随后,方法executeStringInsert返回一个JDBC的ResultSet,用来返回ID或者刚插入的数据行。这个信息可以手动处理,不过Amber提供了辅助函数来把新的ID注入到新对象中。

比起操作的透明度,读写时使用字符串来处理的类型安全问题更容易让人担心。因为把参数转化成字符串后,类型信息就丢失了。然而,这种技术有一个很大的优势:任何查询字符串都会被记录下来,数据库管理员可以通过分析来找出错误原因,并且准确知道应用调用了什么或者从数据库查询了什么。这种类型的透明使得调试更加容易。

如果Jedi的战斗机列表改变了,还是手动更新数据库比较好。取决于Fighter列表发生的变化,比较粗鲁的做法是删除这个Jedi的全部战斗机列表,然后把新的列表写回数据库中。假设我们手里有一个jedi对象和一列新的Fighter对象,我们接下来需要把新列表写进fighters中。更进一步,我们假设通过存储过程InsertFighter把一个新的Fighter对象写进数据库。
Connector connector = 
   ConnectorFactory.createConnector( "starwars" );
BeanWriter writer =
   new BeanWriter( Fighter.class, connector );
connector.execute(
   "delete from fighters where jedi_id = " + jedi.getId() );
for( Fighter fighter : fighters ) {
   fighter.setJediId( jedi.getId() );
   connector.executeStringInsert(
      fighter, "InsertFighter" )
}


这段代码处理一整套的执行字符串,每个字符串中的name分别对应着fighters表中的fighter:
InsertFighter @jediId=..., @name="...";


你也许注意到了,这个方法并没动用事务。像上面说的那样,这里并没使用异常处理,如果delete操作失败了,会产生一个SQLException,而后面的循环根本不会被执行。可是如果是其他情况呢?比如接下来的InsertFighter调用出错了呢?这时事务是必须的,最好把操作放在存储过程里面。如果我想在事务中从Fighter对象获取全部参数以及Jedi ID并处理“新”战斗机呢?这个话题值得在另一篇文章中讨论。

局限和缺点

像任何工具或者技术一样,我们讨论的方法具有一定的局限性。
&#8226;        因为不使用XML描述符,所以当数据库和对象域之间的接口发生改变时,就会出问题。实际上,当改变只发生在名字而不是类型时,或者没有属性/字段增减时,使用XML描述符比Amber好一点。如果不是上述情况,两种系统都需要重新编译和部署。
&#8226;        复合管理不是自动化的。事实上,当你比较Amber和大的O/R框架时,你会发现有很多东西都不是自动化的。在把数据库作为哑存储设备或者用表连接中间层的商业设定中,Amber并没有太大的用处。另一方面,你可以说Amber适合依赖注入风格的设计,以及数据之间的松耦合,这通常认为比隐式依赖要好。
&#8226;        最后,注解分析以及自省机制的运行开销比较大。在一个与数据库有大量交互(比如,一个用于并发用户交互的中间件)而不是单用户或者少量用户偶尔交互的系统中,Amber会导致性能问题。

结论

这篇文章示范了一种相对于传统的O/R映射相反的R/O映射。所谓的面向对象和关系系统之间的阻抗不匹配,在把关系数据模型定义成对象域的引用模型,以及使用存储过程这一工具来操作数据库(尤其是写操作)之后,这个复杂的映射任务被简化了。这种映射是通过注解这种Java 1.5的新语言特性来实现的。我们通过Amber框架来支持和演示了这种方法。

Amber是一个小型框架,易于学习和使用。只需要处理几个非常接近JDBC的类。数据库和JavaBean之间的连接通过注解来实现,不需要任何XML描述符,因为XML对人来说可读性不高。而数据库和应用之间的映射也都在bean类之中。Amber也提供了一种强制检测机制来验证内容,不过为了节约篇幅,就不在这篇文章中讨论了。

Amber只做了一件事并且做得很好:把数据库的列以及查询参数映射到JavaBean的属性。不多,也不少。Amber不是银弹,也没有解决那些庞大的工业O/R框架才能处理的问题。

Amber已经在一个商业环境中证明了它的价值。在Impetus,我们为一家德国最大的邮购公司提供了销售人员解决方案,系统基于Java,使用了MS SQL Server,而我们使用Amber处理了全部的数据库交互。自从今年春天(自从J2SE 5.0的到来)以来,我们没有改变一点API,而且使用中也没出现什么大问题。


版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接
作者:Norbert Ehrekedeafwolf(作者的blog:http://blog.matrix.org.cn/page/deafwolf)
原文:http://www.matrix.org.cn/resource/article/44/44381_Amber+Object+Mapping.html
关键字:relational;mapping;Amber