这个思考源于最近项目中对 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 等语言实现,代码可能写不了那么漂亮,不过也可以做得优雅一些,比如这种链式调用:
new 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 age > #{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 层的设计上亦如此。权衡的技巧。没有通用的和完美的解决办法,只有适合和不适合一说而已。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》