做自己的ORM,不将就,就是挑剔!

作者: leeyaf | 来源:发表于2016-06-25 23:36 被阅读920次

    写在前面

    一直以来都对各种数据库的ORM框架抱以将就的心态,用起来麻烦不顺手,于是我就手动做了一个,并写下这篇文章。

    轻松的阅读本文你需要:

    • 有使用ORM框架的经验,比如Hibernate、Mybatis等。
    • 熟悉commons-dbutils工具类。
    • 了解Java反射技术。
    • 一颗对技术不将就、有追求的心。

    一般的ORM都有什么?

    拿最出名的Hibernate举例子,最方便的地方就是可以直接通过实体进行更新、删除、新增等操作;查询完成后会自动转换为实体;对于hql和sql那个更好,个人觉得sql更好,因为不用在写完sql测试完成后再手动转换为hql;对于实体关联,连表查询结果使用框架转换为实体,个人是不喜欢,因为有更方便更高效的做法。

    从改造dbutils开始

    Apache的开源项目commons-dbutils提供了一些简单的方法,帮助我们完成数据库与程序的交互。其中最为重要的就是,把数据库的返回结果转换成实体对象。

    但是这个方法比较基础,默认数据库的列名要与实体的字段名一致。而我们实际的情况一般是,数据库的列名是user_name、实体的字段名是userName,为了让dbutils转换实体的时候遵守这种约定,需要对dbutils进行改造。

    public class CustomBeanProcessor extends BeanProcessor{
      @Override
      protected int[] mapColumnsToProperties(ResultSetMetaData rsmd, PropertyDescriptor[] props) throws SQLException {
          int cols = rsmd.getColumnCount();
          int[] columnToProperty = new int[cols + 1];
          Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);
          for (int col = 1; col <= cols; col++) {
              String columnName = rsmd.getColumnLabel(col);
              if (null == columnName || 0 == columnName.length()) {
                columnName = rsmd.getColumnName(col);
              }
              String propertyName = SqlHelper.camelConvertColumnName(columnName);  // 只需要修改这一行代码
              if (propertyName == null) {
                  propertyName = columnName;
              }
              for (int i = 0; i < props.length; i++) {
                  if (propertyName.equalsIgnoreCase(props[i].getName())) {
                      columnToProperty[col] = i;
                      break;
                  }
              }
          }
          return columnToProperty;
      }
    }
    

    新建上面的类,继承自dbutils的BeanProcessor,重写mapColumnsToProperties方法,代码完全拷贝,只需要修改上面加注释的一行代码,功能类似把字符串user_name转换成userName,第一步完成。

    public class CustomBasicRowProcessor extends BasicRowProcessor{ 
        public CustomBasicRowProcessor() {
          super(new CustomBeanProcessor());
        }
    }
    

    新建上面的类,继承自dbutils的BasicRowProcessor,没有其他的方法,只是在初始化的时候使用我们自己创建的CustomBeanProcessor,到此dbutils改造完成。

    数据库连接

    对数据库所有操作都是从获取数据库链接开始的,一般叫做Connection或者Session。而获取链接之前你需要先配置数据库连接,一般需要的几个必要条件是 数据库的地址、用户名、密码,这里暂时使用MysqlDataSource进行配置链接。

    private MysqlDataSource getDataSource(){
      MysqlDataSource dataSource=new MysqlDataSource();
      try {
        dataSource.setURL("jdbc:mysql://127.0.01:3306/test");
        dataSource.setUser("admin");
        dataSource.setPassword("password");
        dataSource.setCharacterEncoding("utf-8");
        dataSource.setConnectTimeout(30000);
      } catch (Exception e) {
        e.printStackTrace();
      }
      return dataSource;
    }
    

    有了数据库配置之后就可以获取数据库连接。

    public Connection getConnection() throws Exception{
      return dataSource.getConnection();
    }
    

    当然还有关闭数据库连接,开启事务,回滚事务等。

    public void close(Connection connection){
      try {
        DbUtils.close(connection);
      } catch (SQLException e) {
        e.printStackTrace();
      }
    }
    public void rollback(Connection connection){
      try {
        DbUtils.rollback(connection);
      } catch (SQLException e) {
        e.printStackTrace();
      }
    }
    // 开启事务 connection.setAutoCommit(false);  
    

    查询和更新

    新增、更新和删除对数据库来说都是更新操作,所以这里只提供了两个方法,新增返回插入数据库的id,更新和删除返回受影响的行数。

    private final QueryRunner queryRunner=new QueryRunner();
    public int executeUpdate(String sql,List<?> params,Connection connection,boolean rowId,boolean close) throws Exception{
        try {
            PreparedStatement pstm=connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
            int index=1;
            for (Object object : params) {
                pstm.setObject(index++, object);
            }
            int effectCount=pstm.executeUpdate();
            if(rowId){
                ResultSet rs=pstm.getGeneratedKeys();
                if(rs.next()) return rs.getInt(1);
            }
            else return effectCount;
            return -1;
        } catch (Exception e) {
            throw e;
        } finally {
            if (close) close(connection);
        }
    }
    public <T> T executeQuery(String sql,List<?> params,ResultSetHandler<T> handler, Connection conn, boolean close) throws Exception{
        try {
            return queryRunner.query(conn, sql, handler, params.toArray()); 
        } catch (Exception e) {
            throw e;
        } finally {
            if (close) close(conn);
        }
    }
    

    到这里我们完成了基础的功能,已经可以获取数据库连接、执行简单的sql了。

    像ORM那样去根据实体操作数据库

    前面说到Hibernate可以根据实体去完成新增,更新和删除操作,那具体是怎么做到的呢?当然万变不离其宗,依然是通过sql进行数据库的交互。通过前面做的事情,我们已经可以跑sql了,那么剩下的问题就是,怎么通过实体生成sql语句?Java反射。

    生成新增sql

    遍历实体的所有字段,得到实体的名字和值,自动跳过值为null的字段,int、double等基本数据类型默认都是有值的,不会跳过,我的做法是不使用基本数据类型,使用Integer、Double等的封装数据类型。

    public <T> SqlValue createSaveSql(T entity) throws Exception {
        Class<?> entityClass = entity.getClass();
        StringBuilder builder = new StringBuilder("insert into ");
        String tableName=camelConvertFieldName(entityClass.getSimpleName());
        builder.append(tableName).append(" ( ");
        List<Object> values = new ArrayList<Object>();
        Field[] fields = entityClass.getDeclaredFields();
        for (Field field : fields) {
            String key = camelConvertFieldName(field.getName());
            field.setAccessible(true);
            Object value = field.get(entity);
            if (value==null) continue;
            builder.append(key).append(" , ");
            values.add(value);
        }
        if (values.size()<1) return null;
        builder.delete(builder.lastIndexOf(" , "), builder.length());
        builder.append(" ) values ( ");
        for (int i = 0; i < values.size(); i++) {
            builder.append("? , ");
        }
        builder.delete(builder.lastIndexOf(" , "), builder.length());
        builder.append(" )");
        String sql=builder.toString();
        return new SqlValue(sql, values);
    }
    

    生成更新sql

    默认设定id作为where条件,其他值不为null的字段作为要更新的字段。当然这里自定义了一个@Id的注解,也可以使用第三方的ORM注解。

    public <T> SqlValue createUpdateSql(T entity) throws Exception{
        Class<?> entityClass = entity.getClass();
        StringBuilder builder = new StringBuilder("update ");
        String tableName=camelConvertFieldName(entityClass.getSimpleName());
        builder.append(tableName).append(" set ");
        String idFieldName=null;
        Object idFieldValue=null;
        Field[] fields = entityClass.getDeclaredFields();
        List<Object> values = new ArrayList<Object>();
        for (Field field : fields) {
            String key = camelConvertFieldName(field.getName());
            field.setAccessible(true);
            Object value = field.get(entity);
            if (value==null) continue;
            if (field.isAnnotationPresent(Id.class)) {  // 自定义@Id注解
                idFieldName=key;
                idFieldValue=value;
                continue;
            }
            builder.append(key).append(" = ? , ");
            values.add(value);
        }
        if (values.size()<1) return null;
        builder.delete(builder.lastIndexOf(" , "), builder.length());
        if (idFieldName!=null&&idFieldValue!=null) {
            builder.append(" where ").append(camelConvertFieldName(idFieldName)).append(" = ? ");
        }
        values.add(idFieldValue);
        String sql=builder.toString();
        return new SqlValue(sql, values);
    }
    

    生成删除sql

    实体所有的不为null的字段都作为where条件,一般只传一个id字段。

    public <T> SqlValue createDeleteSql(T entity) throws Exception {
        Class<?> entityClass = entity.getClass();
        StringBuilder builder = new StringBuilder("delete from ");
        String tableName=camelConvertFieldName(entityClass.getSimpleName());
        builder.append(tableName).append(" where ");
        Field[] fields = entityClass.getDeclaredFields();
        List<Object> values = new ArrayList<Object>();
        for (Field field : fields) {
            String key = camelConvertFieldName(field.getName());
            field.setAccessible(true);
            Object value = field.get(entity);
            if (value==null) continue;
            builder.append(key).append(" = ? and ");
            values.add(value);
        }
        if (values.size()<1) return null;
        builder.delete(builder.lastIndexOf(" and "), builder.length());
        String sql=builder.toString();
        return new SqlValue(sql, values);
    }
    

    接收实体

    我们已经可以根据实体生成sql语句了,接下来把数据库连接,执行sql语句的方法联系起来。

    public <T> int save(T entity) throws Exception{
        SqlValue sv=queryStringHelper.createSaveSql(entity);
        Connection connection=getConnection();
        return executeUpdate(sv.getSql(), sv.getValues(), connection,true, true);
    }
    public <T> int update(T entity) throws Exception{
        SqlValue sv=queryStringHelper.createUpdateSql(entity);
        Connection connection=getConnection();
        return executeUpdate(sv.getSql(), sv.getValues(), connection,false, true);
    }
    public <T> int delete(T entity) throws Exception{
        SqlValue sv=queryStringHelper.createDeleteSql(entity);
        Connection connection=getConnection();
        return executeUpdate(sv.getSql(), sv.getValues(), connection,false, true);
    }
    

    传递对象SqlValue的结构如下:

    public class SqlValue {
      private String sql;
      private List<Object> values;
    }
    

    让查询来的更简单一点吧

    上面的executeQuery方法需要提供一个参数ResultSetHandler<T> handler,这个是dbutils的query方法要求传递的对象,用处是把返回结果转换成实体对象。

    private final CustomBasicRowProcessor rowProcessor=new CustomBasicRowProcessor();
    public <T> List<T> getList(String sql,List<?> params) throws Exception{
        Connection connection=getConnection();
        Class<T> entityClass=queryStringHelper.getClassFromSql(sql);
        return executeQuery(sql, params, new BeanListHandler<T>(entityClass, rowProcessor), connection, true);
    }
    public <T> T getOne(String sql,List<?> params) throws Exception{
        Class<T> entityClass=queryStringHelper.getClassFromSql(sql);
        Connection connection=getConnection();
        return executeQuery(sql, params, new BeanHandler<T>(entityClass, rowProcessor), connection, true);
    }
    public <T> T getById(String sql,int id) throws Exception{
        Class<T> entityClass=queryStringHelper.getClassFromSql(sql);
        List<Object> params=new ArrayList<Object>();
        params.add(id);
        Connection connection=getConnection();
        return executeQuery(sql, params, new BeanHandler<T>(entityClass, rowProcessor), connection, true);
    }
    public Long getLong(String sql,List<?> params) throws Exception{
        Connection connection=getConnection();
        return executeQuery(sql, params, new ScalarHandler<Long>(), connection, true);
    }
    

    加入c3p0连接池

    有连接池毕竟是好的,能提升整个框架的相应速度,用ComboPooledDataSource替换之前的MysqlDataSource。

    private final ComboPooledDataSource dataSource=getDataSource();
    private ComboPooledDataSource getDataSource(){
        ComboPooledDataSource pooledDataSource=new ComboPooledDataSource();
        pooledDataSource.setUser("username");
        pooledDataSource.setPassword("password");
        pooledDataSource.setJdbcUrl("url");
        try {
            pooledDataSource.setDriverClass("com.mysql.jdbc.Driver");
        } catch (Exception e) {
            e.printStackTrace();
        }
        pooledDataSource.setInitialPoolSize(3);
        pooledDataSource.setMinPoolSize(3);
        pooledDataSource.setMaxPoolSize(10);
        pooledDataSource.setMaxIdleTime(60);
        pooledDataSource.setMaxStatements(50);
        return pooledDataSource;
    }
    

    实体

    一般的ORM框架都要求一套严谨的实体配置文件,好一点的可以用注解配置,顺便带上各种插件,让实体根据数据库结构自动生成。我使用的是OpenJPA插件,这个插件Eclipse本身就自带,没有复杂的配置文件,配置使用注解实现。

    而上面做的这套框架,无视你的配置文件(除了一个@Id注解),你甚至建一个普通的JavaBean也是可行的。

    结语

    对于缓存,我觉得并没有什么大的必要,因为应用层的缓存粒度比ORM框架层的缓存粒度相对要细的多,所以这里并不加入缓存机制。

    github地址: /leeyaf/orm

    相关文章

      网友评论

      • 简单的土豆:前几个月自己也做了个MVC、ORM框架,不过就是造轮子,为了放在github上增加面试机会 :joy:
        简单的土豆:@leeyaf 二级缓存也可以控制在单个sql级别呀 粒度: 全局->单表表->单SQL, 业务层做的话对于分页,集合这种缓存还是比较麻烦的,使用二级缓存的话 可以减少很多Redis 客户端 get set这样的重复代码(很少一部分需要调用客户端来单独处理)。

        Redis实现二级缓存 > 使用Spring Cache 抽象 > 业务层硬编码,个人认为。

        总之看具体业务吧,这样不一定是最好的,但是可以让开发效率提高很多。
        leeyaf:@酸辣土豆芽 个人感觉业务层缓存粒度要细的多,而且数据库层缓存的更新频率相对过高了,并不能很好的发挥出缓存的作用。
        简单的土豆:@酸辣土豆芽 缓存还是有用的,Mybatis二级缓存结合Redis很爽的,业务层代码零侵入。

      本文标题:做自己的ORM,不将就,就是挑剔!

      本文链接:https://www.haomeiwen.com/subject/jqyadttx.html