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只是在查询的语义下显得更加适合。
————————————————————————————————————-
上面的代码经过了这样三个步骤的演进过程:
- DAO接口和实现全部都要开发人员自己实现;
- 抽象出部分共同的基础增删改查方法不需要实现;
- 将所有实现全部约束到同一个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博客在数日后将不再更新。
上一篇: Flash Scope
下一篇: 以list模拟数据源的分页