DAO的演进

这个思考源于最近项目中对DAO的使用和讨论。数据访问对象,在贫血模型下,要怎样去设计,框架需要完成什么,后续的开发人员需要关注什么,设计的时候到底需要把握怎样的粒度?

最早做项目的时候,是老老实实给每个必要的模型增加DAO接口和实现类的:

public interface IUserDAO{
     public long add(User user);
     public void delete(User user);
     public int count(String condition);
     ... ...
}
public class UserDAOImpl{
}

这样做的好处是针对每个模型都可以自由地扩展和定义想要的数据访问方法,但是明显缺乏控制,每个人实现自己的东西,基础增删改查这种通用的逻辑没有办法规约起来,也没有办法重用起来

查询条件的部分,上面用了一个字符串拼接sql语句的片段传入,这其实是让数据层的东西泄漏到业务层去了,不是一个好的实现;但是也要看到,对于复杂的查询方案,这又是比较容易实现的。

————————————————————————————————————-

后来做了一些改进,采用了下面这种DAO模型:

IBaseDAO ← BaseDAOImpl
      ↑                      ↑
IUserDAO ← UserDAOImpl

IUserDAO实现IBaseDAO接口,同时BaseDAOImpl是IBaseDAO的一个增删改查的基本实现,而UserDAOImpl继承自BaseDAOImpl,又实现了IUserDAO接口。

这样一来起码增删改查这样标准的简单操作全部统一起来了,也不需要在各个模型中重新定义。借由iBatis框架,把SQL语句全部放到xml里去,而又因为有了BaseDAOImpl这个通用实现,对于大多数只需要增删改查的模型来说,在实现类中就不需要做任何事情了。

对于条件查询,部分可以通过对模型中字段取值的特殊情况来处理,name取值为null表示不把该字段放入where子句中,否则则作为匹配条件:

< if test = "name != null" >
     AND NAME LIKE '%#{name}'
</ if >

不过把增删改查(CRUD)这样的基础方法(或者可以增加一些其他的方法)放到基类中也存在一些问题。比如有的类其实不需要update方法,但是没有办法,BaseDAOImpl给实现了——换言之,实现或暴露了本不想实现或暴露的方法,这是让DAO的调用者不舒服的地方。

对于复杂的查询,当时我们引入了少量查询对象,避免了DAO的以外的上层去拼接SQL语句。但是查询对象并不总是一个好东西,往往使得整个对象很庞大,设计很臃肿:

Criteria c = session.createCriteria(User. class );
c.add(Restrictions.eq( "name" ,name));
c.add(Restrictions.lt( "age" , 18 ));

如果是某些动态语言,查询对象可以做到优雅一些:

userDAOImpl.query({
     name: 'Jimmy' ,
     desc: {like: '%funny' }
     age:  and(
         {lt:30},
         {gt:18}
     )
});

如果用Java等语言实现,代码可能写不了那么漂亮,不过也可以做得优雅一些,比如这种链式调用:

CriteriaBuilder.eq( "name" , "Jimmy" ).like( "desc" , "%funny" ).and().gt( "age" , 18 ).lt( "age" , 30 ).and0().toCriteria();

————————————————————————————————————-

最近的项目,则是干脆把实现类全部都省了,用Spring对AOP支持的方式,把这些DAO的实现全部指引到一个GenericDAOImpl上了:

public interface IBaseDAO<T>{
     List<T> list(Map<String, String> conditions);
     void create(T object);
     ... ...
}
public class GenericDAOImpl<T> extends DAOSupport implements IBaseDAO<T>{
}

不同的模型DAO可以完成自己各异的查询方法定义,但是最基础的增删改查全部都由IBaseDAO定义,而所有DAO的实现全部都被Spring拦截后指向GenericDAOImpl完成——换言之,不需要写任何DAO的实现类,而且连类定义都免了

但是有利必有弊,除了前面提到的会不得不暴露所有增删改查基础接口的问题,这样的方式还使得对每个DAO做不同的灵活扩展不太容易,而且固定的接口为了通用性可能显得有些啰嗦(比如我在查询时只需要返回一个数的时候,由于查询接口被定义为返回一个对象的链表,所以被迫要把这个数封装到对象里,再塞进一个链表中返回),当然这也算是框架给开发人员带来的约束力。

值得一提的是,查询条件呢?这次用一个Map来承载,看起来这样查询条件的控制就比较灵活,比如:

map.put( "name" , "Jimmy" );
map.put( "ageGreatThan" , "18" );

而这样的map业务语义只有到了存储查询sql的xml中才能被理解,例如上面的条件也许会变成这样的子句:

where name like '%#{name}'
<if test= "ageGreatThan != null" >
     and ang > #{ageGreatThan}
</if>

总之,相较于查询对象,用map的方式就要自如得多。但是有利必有弊,map方式也存在一些问题,比如多数情况下嵌套层次不如对象易于理解,比如说对开发人员的约束力弱,实现可能五花八门,而且如果拼写错误,在insert/update/delete操作的时候后果会尤其严重。举例来说,有这样一条SQL:

delete from user u
where
< if test = "name != null" >
u.name = '#{name}'
</ if >
... ...

要根据用户名字来删除记录,如果匹配该条件的参数写错了,比如写成这样(多写了一个“s”):

map.put( "names" , "Jimmy" );

就失去了通过该条件寻找被删除条目的能力,导致全表数据被清空。所以通常不建议在update/delete/insert的时候使用map来传递参数,还是考虑对象方式传参优先,map只是在查询的语义下显得更加适合。

————————————————————————————————————-

上面的代码经过了这样三个步骤的演进过程:

  1. DAO接口和实现全部都要开发人员自己实现;
  2. 抽象出部分共同的基础增删改查方法不需要实现;
  3. 将所有实现全部约束到同一个DAOImpl中,开发人员只需要实现各个模型的DAO接口。

看起来逐步地后续开发人员的工作似乎越来越少了,那么能不能达成终极的第4步,把这个工作全部省去,让DAO层完全由框架自动完成呢?

其实也是可以的,只是这个时候DAO方法的执行只能被约束在比较有限的几个增删改查基础方法之内了,这样的DAO是完全不具备业务语义的——换言之,真正将业务逻辑从DAO解耦出去了。

这种情况下后续的开发人员只需要完成存放SQL的xml文件,如果命名按照规约来办,连这个存放SQL的xml文件都可以省去(请参见Grails利用Hibernate自动生成数据库、增删改查的SQL语句,自动完成OR mapping的过程),只是,很多情况下看起来美好而已,这样的解耦未必是一件好事:我们始终要在各种利弊的分析和选择中权衡,如果因为性能等原因需要涉及到联表查询怎么做?业务语义已经不能侵入DAO层了,那么只能以某种方式在DAO外上方的Service来实现条件的拼装,可以用代码来实现,也可以用某种自定义的DSL来实现,这又容易显得过于臃肿了。

所以,兼容也好,灵活也好,都要讲究个度,在DAO层的设计上亦如此。权衡的技巧。没有通用的和完美的解决办法,只有适合和不适合一说而已。

文章系本人原创,转载请注明作者和出处(http://www.raychase.net

注:本博客已经迁移到个人站点 http://www.raychase.net/ ,欢迎大家访问收藏,本ITEye博客在数日后将不再更新。

猜你喜欢

转载自raychase.iteye.com/blog/1688293
DAO
今日推荐