日期:2014-05-16  浏览次数:20404 次

jdbc还是ibatis?
公司的一个大系统的持久层一直是直接使用jdbc。在jdbc的基础上,又自制了一个简陋的cache。

每个持久功能的实现都比较类似,大致相当于这样:
MyProprietaryConnection conn = ConnectionManager.checkOut(Database.DB_NAME);
try {
  PreparedStatement stmt = conn.getPreparedStatement("some statement id, identifying a sql statement in an xml file");
  stmt.setString(1, "param 1");
  stmt.setInt(2, param2);
  ...
  try {
    ResultSet resultSet = stmt.executeQuery();
    try{
      while(resultSet.next()) {
        ...
      }
    }
    finally {
      resultSet.close();
    }
  }
  finally {
    stmt.close();
  }
}
finally {
  ConnectionManager.checkIn(conn);
}

当然,各个功能的实现不完全一样,有的有事务,有的没有;有的忘了关闭statement,有的忘了checkIn connection;有的在出现Error的时候忘了rollback。等等等等。

dao层的代码就是调用这些不同的jdbc代码,然后再包上一层HashMap做cache:
Object cacheKey = ...;
synchronized(cache) {
  Account acct = (Account)cache.get(cacheKey);
  if(acct == null) {
    acct = runJdbcForAccount(...);
    cache.put(cacheKey, acct);
  }
  return acct.cloneAccount();
}

当然,还要自己实现cloneAccount()。
所有对Account, Contribution, Plan之类的cache代码也类似。

后来鉴于偶尔出现资源泄漏问题,一个程序员写了一个jdbc模板,长成这个样子:
abstract class PersisterCommand {
  protected abstract void populateStatement(PreparedStatement stmt);
  protected abstract Object processResult(ResultSet resultSet);
  protected abstract boolean isTransactional();
  protected abstract PreparedStatement getStatement();
  public Object run() {
    MyProprietaryConnection conn = ConnectionManager.checkOut(Database.DB_NAME);
    try {
      PreparedStatement stmt = getStatement();
      populateStatement(stmt);
      ...
      try {
        if(isTransactional()) {
          conn.startTransaction();
        }
        ResultSet resultSet = stmt.executeQuery();
        try{
          Object result = processResult(resultSet);
          if(isTransactional()) {
            conn.commitTransaction();
          }
          return result;
        }
        catch(Exception e){
          if(isTransactional()) conn.rollbackTransaction();
          throw e;
        }
        finally {
          resultSet.close();
        }
      }
      finally {
        stmt.close();
      }
    }
    finally {
      ConnectionManager.checkIn(conn);
    }
  }
}

然后上面的代码可以简化为仅仅重载这四个抽象函数:
getStatement负责取得某个特定的sql statement;populateStatement负责填充参数;processResult负责把ResultSet转换成domain object;isTransactional制定是否使用事务。

介绍了这么多背景情况,希望你已经看到了,原来的直接jdbc方法是非常繁琐,容易出错,代码量大,而且重复很多。
这个PersisterCommand也有很多局限:
1。它只能处理一个connection一个statement,不能做batch。
2。它在Error出现的时候没有rollback。
3。子类仍然要针对jdbc api写一些有重复味道的代码。
4。代码不容易单元测试。因为ConnectionManager.checkOut()和ConnectionManager.checkIn()都是写死的。


另外,这个自制的cache也是一个重复代码的生产者。

针对这种情况,我本来想继续重构,弄出一个CacheManager和更灵活的jdbc模板。但是后来一想,倒还不如直接用ibatis来得好。毕竟ibatis已经是被业界广泛使用的工具,总比自己制造轮子强。而且,相比于hibernate,ibatis也有更贴近我们现在的模型和使用习惯的优势。


我的一个同事(公司的元老),开始是对ibatis很感兴趣的。

可惜的是,当我完成了ibatis的集成,他试用了一下之后就改变了主意。这个同事在项目组甚至整个公司说话都是很有分量的,不说服他,推广ibatis就面临夭折的可能。

我的ibatis的集成长成这个样子:
public interface Action {
  Object run(SqlMapSession session);
}
public class IbatisPersistence {
  public SqlMapSession openSession();
  public Object queryForObject(String key);
  public List queryForList(String key);
  public int update(String key, boolean useTransaction);
  public int delete(String key, boolean useTransaction);
  public int update(String key);
  public int delete(String key);
  public Object run(Action action);
}


这样,除非用户代码调用openSession(),其它的函数都自动处理了Session的关闭。事务处理用一个boolean参数来控制也相当简单。

上面的那么多jdbc代码和cache代码最终就可以直接变成:
Accunt acct = persistence.queryForObject("getAccountById", accountId);


那么同事对这个东西的意见在哪里呢?
1。他和另外一个同事为调试一个使用了ibatis的程序bug花了一天时间。后来把ibatis删掉,直接用jdbc就修好了。