做自己的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> 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

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

推荐阅读更多精彩内容