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

写在前面

一直以来都对各种数据库的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 executeQuery(String sql,List params,ResultSetHandler 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  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 values = new ArrayList();
    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  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 values = new ArrayList();
    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  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 values = new ArrayList();
    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  int save(T entity) throws Exception{
    SqlValue sv=queryStringHelper.createSaveSql(entity);
    Connection connection=getConnection();
    return executeUpdate(sv.getSql(), sv.getValues(), connection,true, true);
}
public  int update(T entity) throws Exception{
    SqlValue sv=queryStringHelper.createUpdateSql(entity);
    Connection connection=getConnection();
    return executeUpdate(sv.getSql(), sv.getValues(), connection,false, true);
}
public  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 values;
}
 
 

让查询来的更简单一点吧

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

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

你可能感兴趣的:(做自己的ORM,不将就,就是挑剔!)