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

寫在前面

一直以來都對(duì)各種數(shù)據(jù)庫的ORM框架抱以將就的心態(tài),用起來麻煩不順手,于是我就手動(dòng)做了一個(gè),并寫下這篇文章。

輕松的閱讀本文你需要:

  • 有使用ORM框架的經(jīng)驗(yàn),比如Hibernate、Mybatis等。
  • 熟悉commons-dbutils工具類。
  • 了解Java反射技術(shù)。
  • 一顆對(duì)技術(shù)不將就、有追求的心。

一般的ORM都有什么?

拿最出名的Hibernate舉例子,最方便的地方就是可以直接通過實(shí)體進(jìn)行更新、刪除、新增等操作;查詢完成后會(huì)自動(dòng)轉(zhuǎn)換為實(shí)體;對(duì)于hql和sql那個(gè)更好,個(gè)人覺得sql更好,因?yàn)椴挥迷趯懲阺ql測試完成后再手動(dòng)轉(zhuǎn)換為hql;對(duì)于實(shí)體關(guān)聯(lián),連表查詢結(jié)果使用框架轉(zhuǎn)換為實(shí)體,個(gè)人是不喜歡,因?yàn)橛懈奖愀咝У淖龇ā?/p>

從改造dbutils開始

Apache的開源項(xiàng)目commons-dbutils提供了一些簡單的方法,幫助我們完成數(shù)據(jù)庫與程序的交互。其中最為重要的就是,把數(shù)據(jù)庫的返回結(jié)果轉(zhuǎn)換成實(shí)體對(duì)象。

但是這個(gè)方法比較基礎(chǔ),默認(rèn)數(shù)據(jù)庫的列名要與實(shí)體的字段名一致。而我們實(shí)際的情況一般是,數(shù)據(jù)庫的列名是user_name、實(shí)體的字段名是userName,為了讓dbutils轉(zhuǎn)換實(shí)體的時(shí)候遵守這種約定,需要對(duì)dbutils進(jìn)行改造。

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轉(zhuǎn)換成userName,第一步完成。

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

新建上面的類,繼承自dbutils的BasicRowProcessor,沒有其他的方法,只是在初始化的時(shí)候使用我們自己創(chuàng)建的CustomBeanProcessor,到此dbutils改造完成。

數(shù)據(jù)庫連接

對(duì)數(shù)據(jù)庫所有操作都是從獲取數(shù)據(jù)庫鏈接開始的,一般叫做Connection或者Session。而獲取鏈接之前你需要先配置數(shù)據(jù)庫連接,一般需要的幾個(gè)必要條件是 數(shù)據(jù)庫的地址、用戶名、密碼,這里暫時(shí)使用MysqlDataSource進(jìn)行配置鏈接。

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;
}

有了數(shù)據(jù)庫配置之后就可以獲取數(shù)據(jù)庫連接。

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

當(dāng)然還有關(guān)閉數(shù)據(jù)庫連接,開啟事務(wù),回滾事務(wù)等。

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();
  }
}
// 開啟事務(wù) connection.setAutoCommit(false);  

查詢和更新

新增、更新和刪除對(duì)數(shù)據(jù)庫來說都是更新操作,所以這里只提供了兩個(gè)方法,新增返回插入數(shù)據(jù)庫的id,更新和刪除返回受影響的行數(shù)。

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);
    }
}

到這里我們完成了基礎(chǔ)的功能,已經(jīng)可以獲取數(shù)據(jù)庫連接、執(zhí)行簡單的sql了。

像ORM那樣去根據(jù)實(shí)體操作數(shù)據(jù)庫

前面說到Hibernate可以根據(jù)實(shí)體去完成新增,更新和刪除操作,那具體是怎么做到的呢?當(dāng)然萬變不離其宗,依然是通過sql進(jìn)行數(shù)據(jù)庫的交互。通過前面做的事情,我們已經(jīng)可以跑sql了,那么剩下的問題就是,怎么通過實(shí)體生成sql語句?Java反射。

生成新增sql

遍歷實(shí)體的所有字段,得到實(shí)體的名字和值,自動(dòng)跳過值為null的字段,int、double等基本數(shù)據(jù)類型默認(rèn)都是有值的,不會(huì)跳過,我的做法是不使用基本數(shù)據(jù)類型,使用Integer、Double等的封裝數(shù)據(jù)類型。

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

默認(rèn)設(shè)定id作為where條件,其他值不為null的字段作為要更新的字段。當(dāng)然這里自定義了一個(gè)@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

實(shí)體所有的不為null的字段都作為where條件,一般只傳一個(gè)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);
}

接收實(shí)體

我們已經(jīng)可以根據(jù)實(shí)體生成sql語句了,接下來把數(shù)據(jù)庫連接,執(zhí)行sql語句的方法聯(lián)系起來。

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);
}

傳遞對(duì)象SqlValue的結(jié)構(gòu)如下:

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

讓查詢來的更簡單一點(diǎn)吧

上面的executeQuery方法需要提供一個(gè)參數(shù)ResultSetHandler<T> handler,這個(gè)是dbutils的query方法要求傳遞的對(duì)象,用處是把返回結(jié)果轉(zhuǎn)換成實(shí)體對(duì)象。

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連接池

有連接池畢竟是好的,能提升整個(gè)框架的相應(yīng)速度,用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;
}

實(shí)體

一般的ORM框架都要求一套嚴(yán)謹(jǐn)?shù)膶?shí)體配置文件,好一點(diǎn)的可以用注解配置,順便帶上各種插件,讓實(shí)體根據(jù)數(shù)據(jù)庫結(jié)構(gòu)自動(dòng)生成。我使用的是OpenJPA插件,這個(gè)插件Eclipse本身就自帶,沒有復(fù)雜的配置文件,配置使用注解實(shí)現(xiàn)。

而上面做的這套框架,無視你的配置文件(除了一個(gè)@Id注解),你甚至建一個(gè)普通的JavaBean也是可行的。

結(jié)語

對(duì)于緩存,我覺得并沒有什么大的必要,因?yàn)閼?yīng)用層的緩存粒度比ORM框架層的緩存粒度相對(duì)要細(xì)的多,所以這里并不加入緩存機(jī)制。

github地址: /leeyaf/orm

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容