您的位置:首页 > 编程语言 > Java开发

spring数据访问对象(dao)框架入门

2013-12-04 09:33 281 查看
摘要

  j2ee应用程序中的业务组件通常使用jdbc api访问和更改关系数据库中的持久数据。这经常导致持久性代码与业务逻辑发生混合,这是一种不好的习惯。数据访问对象(dao)设计模式通过把持久性逻辑分成若干数据访问类来解决这一问题。

  本文是一篇关于dao设计模式的入门文章,突出讲述了它的优点和不足之处。另外,本文还介绍了spring 2.0 jdbc/dao框架并示范了它如何妥善地解决传统dao设计中的缺陷。

  传统的dao设计

  数据访问对象(dao)是一个集成层设计模式,如core j2ee design pattern 图书所归纳。它将持久性存储访问和操作代码封装到一个单独的层中。本文的上下文中所提到的持久存储器是一个rdbms.

  这一模式在业务逻辑层和持久存储层之间引入了一个抽象层,如图1所示。业务对象通过数据访问对象来访问rdbms(数据源)。抽象层改善了应用程序代码并引入了灵活性。理论上,当数据源改变时,比如更换数据库供应商或是数据库的类型时,仅需改变数据访问对象,从而把对业务对象的影响降到最低。



  图1. 应用程序结构,包括dao之前和之后的部分

  讲解了dao设计模式的基础知识,下面将编写一些代码。下面的例子来自于一个公司域模型。简而言之,这家公司有几位员工工作在不同的部门,如销售部、市场部以及人力资源部。为了简单起见,我们将集中讨论一个称作“雇员”的实体。

  针对接口编程

  dao设计模式带来的灵活性首先要归功于一个对象设计的最佳实践:针对接口编程(p2i)program to interface。这一原则规定实体必须实现一个供调用程序而不是实体自身使用的接口。因此,可以轻松替换成不同的实现而对客户端代码只产生很小的影响。

  我们将据此使用findbysalaryrange()行为定义employee dao接口,iemployeedao.业务组件将通过这个接口与dao交互:

  它们将检索到的数据打包到一个与jdbc api无关的传输对象中,然后将其返回给业务层作进一步处理。

  它们实质上是无状态的。唯一的目的是访问并更改业务对象的持久数据。

  在这个过程中,它们像sqlexception一样捕获任何底层jdbc api或数据库报告的错误(例如,数据库不可用、错误的sql句法)。dao对象再次使用一个与jdbc无关的自定义运行时异常类dbexception,通知业务对象这些错误。

  它们像connection和preparedstatement对象那样,将数据库资源释放回池中,并在使用完resultset游标之后,将其所占用的内存释放。

  因此,dao层将底层的数据访问api抽象化,为业务层提供了一致的数据访问api.

  构建dao工厂

  dao工厂是典型的工厂设计模式实现,用于为业务对象创建和提供具体的dao实现。业务对象使用dao接口,而不用了解实现类的具体情况。dao工厂带来的依赖反转(dependency inversion)提供了极大的灵活性。只要dao接口建立的约定未改变,那么很容易改变dao实现(例如,从straight jdbc实现到基于kodo的o/r映射),同时又不影响客户的业务对象:

  代码重复:从employeedaoimpl清单可以清楚地看到,对于基于jdbc的传统数据库访问,代码重复(如上面的粗体字所示)是一个主要的问题。一遍又一遍地写着同样的代码,明显违背了基本的面向对象设计的代码重用原则。它将对项目成本、时间安排和工作产生明显的副面影响。

  耦合:dao代码与jdbc接口和核心collection耦合得非常紧密。从每个dao类的导入声明的数量可以明显地看出这种耦合。

  资源耗损:依据employeedaoimpl类的设计,所有dao方法必须释放对所获得的连接、声明、结果集等数据库资源的控制。这是危险的主张,因为一个编程新手可能很容易漏掉那些约束。结果造成资源耗尽,导致系统停机。

  错误处理:jdbc驱动程序通过抛出sqlexception来报告所有的错误情况。sqlexception是检查到的异常,所以开发人员被迫去处理它,即使不可能从这类导致代码混乱的大多数异常中恢复过来。而且,从sqlexception对象获得的错误代码和消息特定于数据库厂商,所以不可能写出可移植的dao错误发送代码。

  脆弱的代码:在基于jdbc的dao中,两个常用的任务是设置声明对象的绑定变量和使用结果集检索数据。如果sql where子句中的列数目或者位置更改了,就不得不对代码执行更改、测试、重新部署这个严格的循环过程。

  让我们看看如何能够减少这些问题并保留dao的大多数优点。

  进入spring dao

  先识别代码中发生变化的部分,然后将这一部分代码分离出来或者封装起来,就能解决以上所列出的问题。spring的设计者们已经完全做到了这一点,他们发布了一个超级简洁、健壮的、高度可伸缩的jdbc框架。固定部分(像检索连接、准备声明对象、执行查询和释放数据库资源)已经被一次性地写好,所以该框架的一部分内容有助于消除在传统的基于jdbc的dao中出现的缺点。

  图2显示的是spring jdbc框架的主要组成部分。业务服务对象通过适当的接口继续使用dao实现类。jdbcdaosupport是jdbc数据访问对象的超类。它与特定的数据源相关联。spring inversion of control (ioc)容器或beanfactory负责获得相应数据源的配置详细信息,并将其与jdbcdaosupport相关联。这个类最重要的功能就是使子类可以使用jdbctemplate对象。



  图2. spring jdbc框架的主要组件

  jdbctemplate是spring jdbc框架中最重要的类。引用文献中的话:“它简化了jdbc的使用,有助于避免常见的错误。它执行核心jdbc工作流,保留应用代码以提供sql和提取结果。”这个类通过执行下面的样板任务来帮助分离jdbc dao代码的静态部分:

  从数据源检索连接。

  准备合适的声明对象。

  执行sql crud操作。

  遍历结果集,然后将结果填入标准的collection对象。

  处理sqlexception异常并将其转换成更加特定于错误的异常层次结构。

  利用spring dao重新编写

  既然已基本理解了spring jdbc框架,现在要重新编写已有的代码。下面将逐步讲述如何解决前几节中提到的问题。

  第一步:修改dao实现类- 现在从jdbcdaosupport扩展出employeedaoimpl以获得jdbctemplate.

  至此,已经说明为了解决传统dao设计中存在的问题,如何封装和概括jdbctemplate类中jdbc代码的静态部分。现在了解一下有关变量的问题,如设置绑定变量、结果集遍历等。虽然spring dao已经拥有这些问题的一般化解决方案,但在某些基于sql的情况下,可能仍需要设置绑定变量。

  在尝试向spring dao转换的过程中,介绍了由于业务服务及其客户机之间的约定遭到破坏而导致的隐蔽运行时错误。这个错误的来源可以追溯到原始的dao.dbctemplate.queryforlist()方法不再返回employeeto实例列表。而是返回一个map表(每个map是结果集的一行)。

  如您目前所知,jdbctemplate基于模板方法设计模式,该模式利用jdbc api定义sql执行工作流。必须改变这个工作流以修复被破坏的约定。第一个选择是在子类中更改或扩展工作流。您可以遍历jdbctemplate.queryforlist()返回的列表,用employeeto实例替换map对象。然而,这会导致我们一直竭力避免的静态代码与动态代码的混合。第二个选择是将代码插入jdbctemplate提供的各种工作流修改钩子(hook)。明智的做法是在一个不同的类中封装传输对象填充代码,然后通过钩子链接它。填充逻辑的任何修改将不会改变dao.

  编写一个类,使其实现在spring框架特定的接口中定义的方法,就可以实现第二个选择。这些方法称为回调函数,通过jdbctemplate向框架注册。当发生相应的事件(例如,遍历结果集并填充独立于框架的传输对象)时,框架将调用这些方法。

  第一步:传输对象

  下面是您可能感兴趣的传输对象。注意,以下所示的传输对象是固定的:

package com.bea.dev2dev.to;

public final class employeeto implements serializable{

private int empno;
private string empname;
private double salary;

/** creates a new instance of employeeto */
public employeeto(int empno,string empname,double salary) {
this.empno = empno;
this.empname = empname;
this.salary = salary;
}
public string getempname() {
return this.empname;
}
public int getempno() {
return this.empno;
}
public double getsalary() {
return this.salary;
}
public boolean equals(employeeto empto){
return empto.empno == this.empno;
}
}

  第二步:实现回调接口

  实现rowmapper接口,填充来自结果集的传输对象。下面是一个例子:

package com.bea.dev2dev.dao.mapper;

import com.bea.dev2dev.to.employeeto;
import java.sql.resultset;
import java.sql.sqlexception;
import org.springframework.jdbc.core.rowmapper;

public class employeetomapper implements rowmapper{

public object maprow(resultset rs, int rownum)
throws sqlexception{
int empno = rs.getint(1);
string empname = rs.getstring(2);
double salary = rs.getdouble(3);
employeeto empto = new employeeto(empno,empname,salary);
return empto;
}
}

  注意实现类不应该对提供的resultset对象调用next()方法。这由框架负责,该类只要从结果集的当前行提取值就行。回调实现抛出的任何sqlexception也由spring框架处理。

  第三步:插入回调接口

  执行sql查询时,jdbctemplate利用默认的rowmapper实现产生map列表。现在需要注册自定义回调实现来修改jdbctemplate的这一行为。注意现在用的是namedparameterjdbctemplate的query()方法,而不是queryforlist()方法:

public class employeedaoimpl extends namedparameterjdbcdaosupport
implements iemployeedao{

public list findbysalaryrange(map salarymap){

namedparameterjdbctemplate daotmplt =
getnamedparameterjdbctemplate();
return daotmplt.query(iemployeedao.find_by_sal_rng, salarymap,
new employeetomapper());
}
}

  spring dao框架对执行查询后返回的结果进行遍历。它在遍历的每一步调用employeetomapper类实现的maprow()方法,使用employeeto传输对象填充最终结果的每一行。

  第四步:修改后的junit类

  现在要根据返回的传输对象测试这些结果。为此要对测试方法进行修改。

public class employeebusinessserviceimpltest extends testcase {

private iemployeebusinessservice empbusiness;
private map salarymap;
list expresult;

// all methods not shown in the listing remain the
// same as in the previous example
private void initexpectedresult() {
expresult = new arraylist();
employeeto to = new employeeto(2,"john",46.11);
expresult.add(to);
}

/**
* test of getemployeeswithinsalaryrange method, of
* class com.bea.dev2dev.business.
* employeebusinessserviceimpl
*/
public void testgetemployeeswithinsalaryrange() {
list result = empbusiness.
getemployeeswithinsalaryrange(salarymap);
assertequals(expresult, result);
}

public void assertequals(list expresult, list result){
employeeto expto = (employeeto) expresult.get(0);
employeeto actualto = (employeeto) result.get(0);
if(!expto.equals(actualto)){
throw new runtimeexception("** test failed **");
}
}
}

  优势

  spring jdbc框架的优点很清楚。我们获益很多,并将dao方法简化到只有几行代码。代码不再脆弱,这要感谢该框架对命名的参数绑定变量的“开箱即用”支持,以及在映射程序中将传输对象填充逻辑分离。

  spring jdbc的优点应该促使您向这一框架移植现有的代码。希望本文在这一方面能有所帮助。它会帮助您获得一些重构工具和知识。例如,如果您没有采用p2i extract interface,那么可以使用重构,从现有的dao实现类创建接口。除此之外,查看本文的参考资料可以得到更多指导。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: