一个简单的通用dao层工具

dao层设计

通用性的思考
要如何做到dao层与数据库和表无关是一个很值得思考的问题,如果想要偷懒,不为每个业务都编写特定的dao层实现,就必须要考虑所有可能会出现的情况。
关于sql语句
其实要谈论的不是通不通用,因为sql语句就已经是最通用,最灵活的数据库操作实现了,因为你可以用它操作任何数据库只要你写出对应的sql语句。
但是正是因为sql语句太灵活,所以编写很麻烦,而且业务的不同导致数据库表的不同,则sql语句也必定会不同,即便一个业务很简单,你也几乎无法重用以前写过的sql语句。

因此我在程序中要做的就是,以java代码的思维去实现数据库操作,并且不为用户暴露过多的实现细节,所谓不暴露过多的细节是指方法的参数不要太复杂,使方法简单易用,同时能满足一些经常出现的业务。
底层当然采用的是拼接sql的形式,最终达到的效果是,即便你不是很懂sql语句,只要你懂java,不写sql,也可以操作数据库。

于是我便有了这个接口的设计:

/**
 * Created by liuruijie on 2017/1/17.
 * 能满足数据库的多种操作的通用的dao层接口
 */
public interface CommonDao<T> {
    /**
     * 根据主键查询一条数据
     * @param tableName 表名
     * @param pkName 主键字段名
     * @param id 值
     * @param type 要转换的返回类型
     * @return 将记录转换成的po类的实例
     */
    T selectById(String tableName, String pkName, String id, Class<T> type);

    /**
     * 根据查询条件查询记录
     * List<Student> students = commonDao
     *                      .selectByCriteria("m_student"
     *                      , commonDao.createCriteria()
     *                              .not().like("id", "2013")
     *                              .between("age", 10, 20)
     *                              .not().eq("gender", "F")
     *                      , Student.class);
     * @param tableName 表名
     * @param criteria 查询条件
     * @param type 类型
     * @return 将记录转换成的po类的实例的列表
     */
    List<T> selectByCriteria(String tableName, Criteria criteria, Class<T> type);

    /**
     * 查询记录数
     * @param tableName 表名
     * @param criteria 查询条件
     * @return 记录数
     */
    long countByCriteria(String tableName, Criteria criteria);

    /**
     * 根据主键删除一条记录
     * @param tableName 表名
     * @param pkName 主键字段名
     * @param id 主键值
     * @return 影响行数 0或1
     */
    int removeById(String tableName, String pkName, String id);

    /**
     * 保存一个对象为一条数据库记录
     * 如果对象主键不存在,则会新建
     * 如果对象主键已经存在,则会更新
     * @param tableName 表名
     * @param pkName 主键字段名
     * @param entity 要保存的对象实体
     * @return 影响行数 0或1
     */
    int save(String tableName, String pkName, T entity);

    /**
     * 查询条件
     */
    interface Criteria{
        /**
         * 使接下来的条件取非
         */
        Criteria not();

        /**
         * 使与下一个条件的连接词变为or,默认为and
         */
        Criteria or();

        /**
         * 相等
         * @param field 字段名
         * @param val 值
         */
        Criteria eq(String field, Object val);

        /**
         * 字符串匹配
         * @param field 字段名
         * @param val 值
         */
        Criteria like(String field, Object val);

        /**
         * 取两个值之间的值
         * @param field 字段名
         * @param val1 值1
         * @param val2 值2
         */
        Criteria between(String field, Object val1, Object val2);

        /**
         * 限制查询记录数
         * @param start 开始位置
         * @param row 记录数
         */
        Criteria limit(int start, int row);

        /**
         * 获取参数列表
         * @return 参数列表
         */
        List<Object> getParam();

        /**
         * 获取拼接好的where条件sql语句
         * @return sql
         */
        StringBuilder getCriteriaSQL();
    }

    /**
     * 让实现类自己实现建立条件的方法
     * @return 查询条件实例
     */
    Criteria createCriteria();
}

这个接口提供了最常用的一些操作:根据主键查询记录,多条件查询,删除一条记录,更新记录以及插入记录,而且都是能满足大多数业务的,并且需要用户提供的参数也不多。
当然要做到简单,就必须要舍弃一些不常见的细节,比如说这里我就只考虑了单主键的情况。
当然这些都可以不断地完善,一开始就做出十全十美的东西是肯定不可能的。
接下来看一下实现:

/**
 * Created by liuruijie on 2017/1/17.
 * 通用dao层接口的实现
 */
@Service
public class CommonDaoImpl<T> implements CommonDao<T>{
    @Resource
    JdbcTemplate jdbcTemplate;

    @Override
    public T selectById(String tableName, String pkName, String id, Class<T> type) {
        Map<String, Object> obj = jdbcTemplate.queryForMap("SELECT * FROM "+tableName+" WHERE "+pkName+" = ?", id);
        return ObjectUtil.mapToObject(obj, type);
    }

    @Override
    public List<T> selectByCriteria(String tableName, CommonDao.Criteria criteria, Class<T> type) {
        StringBuilder sqlStr = new StringBuilder("");
        sqlStr.append("SELECT * FROM ")
                .append(tableName)
                .append(criteria.getCriteriaSQL());
        System.out.println(sqlStr.toString());
        Object[] params = criteria.getParam().toArray(new Object[criteria.getParam().size()]);
        List<Map<String, Object>> objs = jdbcTemplate.queryForList(sqlStr.toString(), params);
        List<T> results = new ArrayList<>();
        for(Map<String, Object> o: objs){
            results.add(ObjectUtil.mapToObject(o, type));
        }
        return results;
    }

    @Override
    public long countByCriteria(String tableName, CommonDao.Criteria criteria) {
        String sql = "SELECT COUNT(*) AS num FROM "+tableName + criteria.getCriteriaSQL();
        Map<String, Object> map = jdbcTemplate.queryForMap(sql, criteria.getParam().toArray());
        return (Long)map.get("num");
    }

    @Override
    public int removeById(String tableName, String pkName, String id) {
        String sql = "DELETE FROM " +
                tableName +
                " WHERE " +
                pkName +
                " = ?";
        return jdbcTemplate.update(sql, id);
    }

    @Override
    public int save(String tableName, String pkName, T entity) {
        Map<String, Object> obj = ObjectUtil.objectToMap(entity);
        StringBuffer sql1 = new StringBuffer("INSERT INTO ")
                .append(tableName)
                .append("(");
        StringBuffer sql2 = new StringBuffer(" VALUES(");
        List<Object> args = new ArrayList<>();
        int count = 0;
        for(String key: obj.keySet()){
            Object arg = obj.get(key);
            if (arg==null){
                continue;
            }
            sql1.append(key).append(",");
            sql2.append("?,");
            args.add(arg);
        }
        sql1.deleteCharAt(sql1.length() - 1);
        sql1.append(") ");
        sql2.deleteCharAt(sql2.length() - 1);
        sql2.append(") ");
        String sql = sql1.append(sql2).toString();
        System.out.println(sql);
        try {
            count += jdbcTemplate.update(sql, args.toArray());
        }catch (DuplicateKeyException e){
            sql1 = new StringBuffer("UPDATE ")
                    .append(tableName)
                    .append(" SET ");
            sql2 = new StringBuffer(" WHERE "+pkName+"=?");
            args = new ArrayList<>();
            for (String key: obj.keySet()){
                if (key.equals(pkName)){
                    continue;
                }
                Object arg = obj.get(key);
                if (arg==null){
                    continue;
                }
                sql1.append(key).append("=?,");
                args.add(arg);
            }

            sql1.deleteCharAt(sql1.length() - 1);
            args.add(obj.get(pkName));
            sql = sql1.append(sql2).toString();
            System.out.println(sql);
            count+=jdbcTemplate.update(sql, args.toArray());
        }
        return count;
    }

    @Override
    public CommonDao.Criteria createCriteria() {
        return new Criteria();
    }


    /**
     * 查询条件的实现
     */
    class Criteria implements CommonDao.Criteria{
        private boolean not; //是否标记了非
        private boolean begin; //是否正在拼接第一个条件
        private boolean or;//是否修改连接词为OR
        StringBuilder criteriaSQL; //从where开始的条件sql
        List<Object> param; //参数列表
        String limitStr; //限制条数

        public Criteria(){
            criteriaSQL = new StringBuilder("");
            param = new LinkedList<>();
            not = false;
            begin = true;
            limitStr = "";
        }

        public Criteria not(){
            not = true;
            return this;
        }

        @Override
        public CommonDao.Criteria or() {
            or = true;
            return this;
        }

        private void link(){
            //判断是否是第一个条件
            // ,如果是就加WHERE不加连接词
            // ,不是就直接加连接词
            if(begin){
                criteriaSQL.append(" WHERE ");
            }else{
                if(or){
                    criteriaSQL.append(" OR ");
                }else{
                    criteriaSQL.append(" AND ");
                }
            }
            or = false;
        }

        public Criteria eq(String field, Object val) {
            link();
            if (not) {
                criteriaSQL.append(field)
                        .append(" != ?");
            } else {
                criteriaSQL.append(field)
                        .append(" = ?");
            }
            not = false;
            begin = false;
            param.add(val);
            return this;
        }

        public Criteria like(String field, Object val){
            link();
            if(not){
                criteriaSQL.append(field)
                        .append(" NOT LIKE ?");
            }else{
                criteriaSQL.append(field)
                        .append(" LIKE ?");
            }
            not = false;
            begin = false;
            param.add("%"+val+"%");
            return this;
        }
        public Criteria between(String field, Object val1, Object val2){
            link();
            if(not){
                criteriaSQL.append(field)
                        .append(" NOT BETWEEN ? AND ? ");
            }else{
                criteriaSQL.append(field)
                        .append(" BETWEEN ? AND ? ");
            }
            not = false;
            begin = false;
            param.add(val1);
            param.add(val2);
            return this;
        }

        @Override
        public CommonDao.Criteria limit(int start, int row) {
            limitStr = " limit " + start + "," + row;
            return this;
        }

        public List<Object> getParam(){
            return this.param;
        }
        public StringBuilder getCriteriaSQL(){
            return new StringBuilder(criteriaSQL.toString()+limitStr);
        }
    }
}

实现使用了spring的jdbcTemplate,其实也可以用最原始的jdbc来实现,虽然麻烦一点,但可以减少依赖。
只要接口设计合理,实现起来无非就是一些字符串的拼接,不会太复杂。这里面用到了map和对象互转的工具,为了偷懒,我两个转换方法的实现我是采用的fastjson中序列化json的方法来转换的。

/**
 * Created by liuruijie on 2017/1/17.
 * 对象工具
 */
public class ObjectUtil {
    /**
     * 对象转字典
     */
    public static Map<String, Object> objectToMap(Object obj){
        return (Map<String, Object>) JSON.toJSON(obj);
    }

    /**
     * 字典转对象
     */
    public static <T> T mapToObject(Map<String, Object> map, Class<T> T){
        return (T) JSON.parseObject(JSON.toJSONString(map), T);
    }
}

写在最后的一些感受
可能会有人觉得,为什么不用ORM框架来做dao层接口,也同样很简单,很方便。
这样说吧,以前我是使用的mybatis来做的数据库访问层,并且使用了它的插件mybatisGenerator来做的根据数据库,逆向生成代码,不得不承认它确实很方便,可同样可以做到不写一条sql语句就能够操作数据库。但是用到后来,每个表对应至少一个po类+一个xml文件+一个dao接口+一个Example条件类,到业务越来越复杂,这些东西也就越来越多,然后你就会发现这种自动生成的代码几乎是不能维护的。而且它为你生成了太多无用的代码,要想精简代码,还是必须要自己写sql语句。
虽然使用流行的框架,不仅让写代码更方便,还能保证稳定性,而且自己写sql语句还能进行优化,提高执行效率,但是请想一想,有多少人能写出高性能的sql语句呢,对于一些普通的业务,如:xxx人员管理系统,xxx图书管理系统等,这些小型项目,高性能的sql语句又能使其得到多大的提升呢。
我的一贯思路都是,实现优先,然后考虑扩展性和可重用性,然后考虑稳定性,最后才考虑性能问题。能让程序在最短的时间内能运行起来才是最重要的,而不是为了提升程序在运行时的速度,而使用复杂的实现,导致迟迟不能运行,最后由于代码考虑的因素过多,把自己搞晕。

到此,我在后端的方向将一个web项目的结构设计从前后端的交互,到业务层的异常处理,再到数据访问层的设计都给出了自己的思路。虽然不是很完美,但是对于我自己来说,这个设计还是挺使用的,这3篇文章中提到的设计思路几乎都是可以运用于各种项目中的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容