被滥用的service+serviceImpl
JAVA大概是从2003年开始流行,我也是从那时开始学习JAVA。在这十多年中,相关技术推陈出新,我切身感受到这些变化。虽然很多程序员不断追随新技术,但未必领悟到这些变化的推动因素。 最近我看到不少新开工的项目,仍然大量采用 “service+serviceImpl、dao+daoImpl” 的代码结构,说真的,我有点痛心,似乎这种做法是理所当然的,似乎这成了一个技术套路。 今天,我想说的是,这样做是不合理的、没有意义的、过时的。
从代码混战到分层分块
由于java的流行和互联网的普及,企业网站、企业应用的开发开始从C/S转为B/S, 所以从那时起做好一个WEB应用很重要。 刚开始,也即2002~2003年,大家都是采用jsp+javabean+jdbc的方式,也不知道怎么分层,有的干脆把所有代码放在jsp中。 后来大家发现这样没法玩了,系统根本没法维护,也不安全,于是就有了MVC这样的架构模式。 MVC的理念挽救了这种局面,并且,运用该理念的模块化开发框架Struts出现了。MVC和Struts的广泛使用,使得开发WEB应用在业界达成了一个共识,那就是WEB应用基本可分为表现层、控制层、业务处理层。
到2004年,对WEB应用进行分层、分模块已是程序员的常识。 但是,有追求有讲究的企业发现了新的问题,那就是如何使自己的应用或产品既可以跑在mysql上,也可以跑在oracle上。 这个问题对程序员来说,就是如何编写业务处理层,使之易于移植到其它数据库。
为解决移植性问题而产生的套路
2005年以前的大多数项目都是直接在业务处理层的Service类中嵌入JDBC代码,这就使得这个Service类与数据库紧藕合,在换一种数据库的情况下,就要修改Service类中的sql。 根据软件设计的开闭原则,软件应该对修改关闭、对扩展开放。 因此,那时聪明的程序员就把这个Service类设计成一个接口,使控制层只依赖这个接口,于是就有了controller+service+serviceImpl;这样,当某天这个应用要跑在其它数据库上时,就而只需要增加一个serviceImpl类。 这就是service+serviceImpl套路产生的背景。
在那时service+serviceImpl并非解决这个问题的唯一方案,还有部分项目,他们的团队更有想法,他们把与数据库打交道的代码从service类中提取出来,成为单独的“数据访问层”(也称为“持久层”),于是形成了这样的层次结构 controller+service+dao+daoImpl。 了不起!这样对不同的数据库,可以有对应的daoImpl。 相比前面那种方案,而扩展一个daoImpl比扩展一个serviceImpl省事多了。
数据访问层的解决方案或框架
由于传统的JDBC代码,繁锁费事,因此不少人或团队尝试将这些代码封装起来,以使程序员不用再编写操作数据库连接、游标、数据集这样的逻辑。这样的工具通常以O/RMapping的思想作为基础,也称为O/RMapping框架。 2006年,hibernate从多种ORMapping框架中脱颖而出,很多项目中的 serviceImpl类开始采用hibernate来实现。hibernate强大,是把双刃剑,易上手但用好难、扩展性好但效率一般。(这也是我曾自研easydb的原因,代码: https://github.com/HuQingmiao/easydb)
2010年,myBatis诞生,2012年开始流行并讯速得到广泛认可。由于myBatis本身是采用xml文件实现的,因此能极好地融入到项目中,只需要把service+dao+daoImpl 中的daoImpl类去掉,改由其mapp.xml实现即可,即service+dao+mapper.xml。 然而,还有很多人在用myBatis的项目中,采用service+serviceImpl+ dao+daoImpl+ mapper.xml, 真的是浪费青春。所以,我说很多人在误用myBatis。
myBatis的极简用法
myBatis天生就是“依赖反转”、“依赖接口编程”的极佳范本,你无需再弄个daoImpl。我建议的简洁用法,思路如下:
一. 先把增、删、查、改的操作抽象成11个标准的接口方法,形成DAO接口的基类。
public interface BasicDao {
public int save(BasicVo basicVo);
public int saveBatch(List list);
public int update(BasicVo basicVo);
public int updateIgnoreNull(BasicVo basicVo);
public int updateBatch(List list);
public int delete(BasicVo basicVo);
public int deleteBatch(List list);
public int deleteByPK(Long id);
public int deleteAll();
public long count();
public BasicVo findByPK(Long id);
public List find(Map<String, Object> paramMap, PageBounds pageBounds);
二. 定义一个VO基类
public abstract class BasicVo implements Serializable {
public BasicVo() {
}
}
三. 为每个数据库表编写Vo类、Dao类、Mapper.xml, 我以Book表为例,编写相应代码:
/**
* Book表对应的Vo类,它继承BasicVo。
*/
public class Book extends BasicVo {
private static final long serialVersionUID = 1L;
private Long bookId;
private String title;
private Double price;
private java.sql.Date publishTime;
private byte[] blobContent;
private String textContent;
public Long getBookId() {
return bookId;
}
public void setBookId(Long bookId) {
this.bookId = bookId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
// 以下省略其它get/set方法
...
}
/**
* Book表对应的Dao类,它继承BasicDao。 注意:这个接口类不需要再声明方法,父接口BasicDao中的11个方法
* 已经能满足我们90%使用场景,对于另外10%的场景,你可以在这里添加你的个性方法。
*/
public interface BookDao extends BasicDao {
}
以下是BookMapper.xml的内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xxx.xxx.xxx.BookDao">
<!-- ============================= INSERT ============================= -->
<insert id="save" useGeneratedKeys="true" keyProperty="id" >
INSERT INTO book( book_id,title,price,publish_time,blob_content,text_content )
VALUES ( #{bookId},#{title},#{price},#{publishTime},#{blobContent},#{textContent})
</insert>
<insert id="saveBatch">
INSERT INTO book( book_id,title,price,publish_time,blob_content,text_content )
<foreach collection="list" item="item" index="index" separator="UNION ALL">
SELECT #{item.bookId},#{item.title},#{item.price},#{item.publishTime},#{item.blobContent},#{item.textContent}
FROM DUAL
</foreach>
</insert>
<!-- ============================= UPDATE ============================= -->
<update id="update">
UPDATE book
<set>
title=#{title},
price=#{price},
publish_time=#{publishTime},
blob_content=#{blobContent},
text_content=#{textContent},
</set>
WHERE book_id=#{bookId}
</update>
<update id="updateIgnoreNull">
UPDATE book
<set>
<if test="title!= null">title=#{title},</if>
<if test="price!= null">price=#{price},</if>
<if test="publishTime!= null">publish_time=#{publishTime},</if>
<if test="blobContent!= null">blob_content=#{blobContent},</if>
<if test="textContent!= null">text_content=#{textContent},</if>
</set>
WHERE book_id=#{bookId}
</update>
<update id="updateBatch" parameterType="java.util.List">
<foreach collection="list" item="item" index="index" separator=";">
UPDATE book
<set>
title=#{item.title},
price=#{item.price},
publish_time=#{item.publishTime},
blob_content=#{item.blobContent},
text_content=#{item.textContent},
</set>
WHERE book_id=#{item.bookId}
</foreach>
</update>
<!-- ============================= DELETE ============================= -->
<delete id="delete">
DELETE FROM book
WHERE book_id=#{bookId}
</delete>
<delete id="deleteBatch">
DELETE FROM book
WHERE
<foreach collection="list" item="item" index="index" open="(" separator="OR" close=")">
(book_id=#{item.bookId} )
</foreach>
</delete>
<delete id="deleteByPK">
DELETE FROM book
WHERE book_id=#{bookId}
</delete>
<delete id="deleteAll">
DELETE FROM book
</delete>
<!-- ============================= SELECT ============================= -->
<select id="count" resultType="java.lang.Long">
SELECT COUNT(1) FROM book
</select>
<select id="findByPK" resultType="Book">
SELECT * FROM book
WHERE book_id=#{bookId}
</select>
<select id="find" resultType="Book">
SELECT *
FROM book
<where>
<if test="bookId!= null">
AND book_id = #{bookId}
</if>
<if test="title!= null">
AND title like #{title}
</if>
<if test="minprice!= null">
AND price >= #{minprice}
</if>
<if test="maxprice!= null">
<![CDATA[ AND price < #{maxprice} ]]>
</if>
<if test="publishTime!= null">
AND publish_time = #{publishTime}
</if>
<if test="blobContent!= null">
AND blob_content = #{blobContent}
</if>
<if test="textContent!= null">
AND text_content = #{textContent}
</if>
</where>
</select>
</mapper>
四. 现在你的service类就可以直接调各dao接口了:
// 增加两条书目
Book[] BookArray = new Book[size];
BookArray[0] = new Book();
BookArray[0].setBookId(new Long(102));
BookArray[0].setTitle(new String("UNIX-上册"));
BookArray[0].setPrice(new Double(40.0));
BookArray[1] = new Book();
BookArray[1].setBookId(new Long(103));
BookArray[1].setTitle(new String("UNIX-中册"));
BookArray[1].setPrice(new Double(60.0));
bookDao.saveBatch(Arrays.asList(BookArray));
// 查询相关书目
HashMap<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("title", "UNIX%");
paramMap.put("minCost", new Float(21));
//取第4条开始的3条记录
PageBounds pageBounds = new PageBounds(4, 3);
List<Book> bookList = bookDao.find(paramMap, pageBounds);
以上代码,非常简洁、易于维护。 并且,我编写了一个代码生成器mybatis-daoj(代码:https://github.com/HuQingmiao/mybatis-daoj ),使得以上这些代码你都不用去写。你只要配置好数据库地址、指定表名,这个工具就能为你生成以上这些代码。
另外要说明的是,虽然以上代码默认为每个表生成一个mapper.xml, 但并不是说不支持多表关联。比如,你要查出某些书及作者的相关信息,你完全可以在BookDao中增加一个接口方法:
public List findBooksAndAuthor(Map<String, Object> paramMap, PageBounds pageBounds);
然后在BookMapper.xml中增加一段:
<select id="findBooksAndAuthor" resultType="BookAuthor">
SELECT a.book_id, a.title, a.price, a.publish_time, b.name, b.sex, b.birthday
FROM book a, author b
<where>
a.author_id = b.id
<if test="title!= null">
AND a.title like #{title}
</if>
</where>
</select>
以上代码中的“BookAuthor”为Vo类名,其实也可以继续用“Book”这个Vo,只需在其中增加'sex', 'birthday' 两个属性即可。