城南大猫

城南大猫

自己动手:做个数据库访问层(一)

       说数据库是信息系统里最重要的部分,应当没几个人反对。最简单的访问数据库的方式就是用程序直连数据库,通过Sql进行操作,相信这也是每个程序员最初学的方法。但随着程序规模的增大,再一条条语句去写的话开发效率就有些低了,因此才有了很多框架去帮助我们操作数据库,比较流行的有Mybatis和Hibernate,也就是所谓的ORM。对于这两个工具我也不是很熟悉,只是接触过网上的一些教程,从网上接触的教程来看,感觉还是有些太复杂了,尤其是对于分页和复杂条件查询。因此就想自己动手做一个,不求大而全,只希望能完成以下功能:

  1. 不用写Sql,能直接保存Java对象,还有更新与删除。

  2. 支持多步查询,比如传递一个Sql数组,第一行的结果可以在第二行的查询中使用,这样就不用手动写复杂的存储过程,生成临时表了,也不用写复杂的子查询,代码更清晰,比如下面的语句  

+zuZhiList select Id from ZuZhi where TypeId = 1
select r.* From RenYuan r join @zuZhiList z on z.Id = r.ZuZhiId

  3. 支持条件查询,比如下面的语句  

+zuZhiList select Id from ZuZhi where 1=1 {@typeId: and TypeId = @typeId}
select r.* From RenYuan r join @zuZhiList z on z.Id = r.ZuZhiId

  执行时以HashMap<String, Object>存储命名参数,如果该Map里没有名为typeId的参数,或者值为Null,那么就不拼接" and TypeId = @typeId"这部分。

  4. 支持自动分页,自动分析Sql语句,如果有分页关键字,就拆分成两条,一条返回总数,一条返回分页记录,比如以下语句  

Select * From RenYuan where ZuZhiId = 1 page 0, 10

  在执行时会被翻译成两条语句  

Select count(*) totalCount From RenYuan where ZuZhiId = 1
Select * From RenYuan where ZuZhiId = 1 limit 0, 10 

  5. 能支持多种数据库,如SqlServer,MySql,Oracle

  实现思路是这样的:

  0. 写几个基础的支持方法,能够连接数据库,运行Sql,如果是查询的话还可以返回多个结果集。

  1. 用反射去做,拼接Sql。

  2. 如果Sql以加号开头,就创建一个临时表,临时表的字段如果Sql里有定义就用Sql里定义的,没有的话就默认为(Id int),然后把后面的select里的内容插入到临时表中。把创建的临时表名称保存在一个集合里,后面的Sql里的参数名称如果在这个集合里,就替换成临时表的名称,没有的话说明是一个普通参数。

  3. 查找语句里用大括号包着的部分,再从第一部分里取出参数名称,查找命名参数里有没有该Key或者值为不为空,如果不为空,就拼接第二部分的Sql,否则就忽略。

  4. 同理,查找关键字,找到From后面的部分,在前面添加select count

  5. 根据不同的语法,替换成不同的内容。

  以下是第0部分的实现:

  定义一个抽象类BaseRepository,将来根据不同的数据库再定义相应的Repository如MySqlRepository,SqlServerRepository,该类里包含两个抽象方法,一个是getDataSource(),即获取连接池的连接,该属性将由Spring注入进来。另外一个是getScriptParser(),返回不同数据库对应的Sql处理类,如MySqlScriptParser,SqlServerScriptParser,另外还有一个DaoUtil类,主要用于处理参数赋值,结果集映射等辅助工作。

public abstract class BaseRepository {
  //获取数据库连接,由Spring注入进来
    protected abstract DataSource getDataSource();
  //获取Sql分析实例

    protected abstract ScriptParser getScriptParser();
  //辅助类

    public abstract DaoUtil getDaoUtil();
  //参数Sql即执行的一条Sql,对于Oracle只能是单条语句,对于MySql和SqlServer,可以是用分号分割开的多条语句,paras即Sql里用?表示的占位参数的值

    protected DataSet executeRetDataSet(String sql, Object... paras) {
    //DataSet为辅助类,包含多个DataTable,DataTable也是辅助类,包含一个名为rows的ArrayList<ArrayList<String, Object>>对象,是从C#借鉴过来的
        DataSet result = new DataSet();
        Connection conn = null;
        Statement stmt = null;
        try {
       conn = DataSourceUtils.getConnection(this.getDataSource());
            ResultSet rs = null;
            if (paras.length == 0) {
                stmt = conn.createStatement();
                stmt.execute(sql);
            } else {
         //此处是根据反射,查看参数的类型,进行特殊处理,如boolean要对应成数据库里的1或者0,Date类型要调用Statement的setTimestamp方法
         PreparedStatement pstmt = getDaoUtil().getCallableStatement(conn, sql, paras); 
         pstmt.execute(); 
         stmt = pstmt; 
        } 
       do { 
         //处理结果集
         if (stmt.getUpdateCount() == -1) {
                    rs = stmt.getResultSet();
                    if (rs != null) {
                        result.tables.add(getDaoUtil().mapResultSetToDataTable(rs));
                        rs.close();
                    }
                }
            } while (stmt.getMoreResults() || stmt.getUpdateCount() > -1); stmt.close();
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } finally {
            if (stmt != null) {
                JdbcUtils.closeStatement(stmt);
            }
            if (conn != null) {
                DataSourceUtils.releaseConnection(conn, this.getDataSource());
            }
        } return result;
    }

    protected void executeNoRet(String sql, Object... paras) {
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = DataSourceUtils.getConnection(this.getDataSource());
            if (paras.length == 0) {
                stmt = conn.createStatement();
                stmt.execute(sql);
            } else {
                PreparedStatement pstmt = getDaoUtil().getCallableStatement(conn, sql, paras);
                pstmt.execute();
                stmt = pstmt;
            }
            stmt.close();
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } finally {
            if (stmt != null) {
                JdbcUtils.closeStatement(stmt);
            }
            if (conn != null) {
                DataSourceUtils.releaseConnection(conn, this.getDataSource());
            }
        }
    }

  //该方法接收一个Sql数组,paras为命名参数,对于同一个参数,Sql里可能出现多次,但paras只需要出现一次就可以。ScriptParser会把Sql里以@开头的参数替换成问号,并且组装出数量顺序一致的用于执行语句的参数数组。
    public DataSet executeQuery(ArrayList<String> scripts, Map<String, Object> paras) {
     //TupleTow为辅助类,类似其他语言里的元组
        TupleTwo<String, ArrayList<Object>> ret = getScriptParser().parseScripts(scripts, paras);
        return executeRetDataSet(ret.getItem1(), ret.getItem2().toArray());
    }

    public <T> void save(T item) {
        Class<T> cls = (Class<T>) item.getClass();
        save(cls, item);
    }

    public <T> void save(Class<T> cls, T item) {
        throw new NotImplementedException();
    }

    public <T> T get(Class<T> cls, int id) {
        throw new NotImplementedException();
    }
}

  

posted on 2018-10-18 16:31  城南大猫  阅读(243)  评论(0)    收藏  举报

导航