spring jdbc 和 iBatis 操作数据库性能分析(一)

基本上每一个APP都会和DB打交道,执行CURD操作,对于用java编写的APP来说CURD的实现方案有很多,但基本上分为两种,ORM和非ORM,。Hiberante为业界所推荐的ORM解决方案,还有很多,自己度娘一下,今天的主题是NO-ORM,不对ORM做讨论。
现在的问题是,如果在项目中不用ORM来做CURD操作就意味着要自己在项目中封装CURD操作。这种情况下,我们会做出如下几种选择:
A 开发人员直接使用Connection,PreparedStatement,Statement,ResultSet等jdbc核心接口和DB交互,当然,如果你的项目很小,扩张的可能性小,用户的交互性不强的情况下,以上做法其实是很方便的 。然而...如果项目很大,需求变更大,扩展的可能性大,用户交互性强,以上方法会非常的让开发与维护人员蛋疼,都懂得!怎么办,请接着看。
B 由对JDBC接口精通的开发人员自己封装工具类,客户程序仅仅传一个connection或者datasource 对象还有一条sql语句和相应参数给工具类就OK了,和DB交互的那套丑陋又必不可少的样板代码交给工具类,这样做也行,但就看工具类的开发人员水平有多高 ,隐患的BUG会让客户程序员蛋疼!项目也会随之崩溃!如果项目里没有N人,怎么办,接着看。
C 我们所遇到的问题,国外的N人们早就深有体会,所以spring jdbc,ibatis,dbutils......就出现了,这些都是开源的。以上开源框架的出现就是为了解决A,B中出现的问题。OK,今天的主题是性能分析,不罗嗦了,直奔主题吧。

场景1 通过给定的SQL,从DB中查询数据

select * from student;
注:在以下的讨论中,都不考虑SQL分页的情况

用spring可以怎么做呢?
a,使用JdbcTemplate中的queryForList(String sql)方法,该方法签名如下

public List<Map<String, Object>> queryForList(String sql) throws DataAccessException {
return query(sql, getColumnMapRowMapper());
}

相应的接口描述如下:

/**
* Execute a query for a result list, given static SQL.
* <p> Uses a JDBC Statement, not a PreparedStatement. If you want to
* execute a static query with a PreparedStatement, use the overloaded
* {@code queryForList} method with {@code null} as argument array.
* <p>The results will be mapped to a List (one entry for each row) of
* Maps (one entry for each column using the column name as the key).
* Each element in the list will be of the form returned by this interface's
* queryForMap() methods.

* @param sql SQL query to execute
* @return an List that contains a Map per row
* @throws DataAccessException if there is any problem executing the query
* @see #queryForList(String, Object[])
*/
List<Map<String, Object>> queryForList(String sql) throws DataAccessException;

返回结果是以Map为元素的List,其中Map是以column name为key,column value为value。与该方法相对应的方法签名如下

/**
* Execute a query for a result Map, given static SQL.
* <p>Uses a JDBC Statement, not a PreparedStatement. If you want to
* execute a static query with a PreparedStatement, use the overloaded
* {@link #queryForMap(String, Object...)} method with {@code null}
* as argument array.
* <p>The query is expected to be a single row query; the result row will be
* mapped to a Map (one entry for each column, using the column name as the key).
* @param sql SQL query to execute
* @return the result Map (one entry for each column, using the
* column name as the key)
* @throws IncorrectResultSizeDataAccessException if the query does not
* return exactly one row
* @throws DataAccessException if there is any problem executing the query
* @see #queryForMap(String, Object[])
* @see ColumnMapRowMapper
*/
Map<String, Object> queryForMap(String sql) throws DataAccessException;
这个方法只是为了返回一条记录,其具体实现实现如下

public Map<String, Object> queryForMap(String sql) throws DataAccessException {
return queryForObject(sql, getColumnMapRowMapper());
}

public <T> T queryForObject(String sql, RowMapper<T> rowMapper) throws DataAccessException {
List<T> results = query(sql, rowMapper);这个倒霉的方法会查询所有数据
return DataAccessUtils.requiredSingleResult(results);

}
DataAccessUtils.requiredSingleResult这个方法的实现个人感觉让人很纠结,实现如下

/**
* Return a single result object from the given Collection.
* <p>Throws an exception if 0 or more than 1 element found.
* @param results the result Collection (can be {@code null})
* @return the single result object
* @throws IncorrectResultSizeDataAccessException if more than one
* element has been found in the given Collection
* @throws EmptyResultDataAccessException if no element at all
* has been found in the given Collection
*/
public static <T> T requiredSingleResult(Collection<T> results) throws IncorrectResultSizeDataAccessException {
int size = (results != null ? results.size() : 0);
if (size == 0) {
throw new EmptyResultDataAccessException(1);
}
if (results.size() > 1) {
throw new IncorrectResultSizeDataAccessException(1, size);
}
return results.iterator().next();
}

requiredSingleResult()方法的目的就是为了验证记录数有且只有一条!
所以在使用queryForMap(String sql)方法要小心,如果你传了一条查询某张表所有数据的sql给这个方法,并且此表的数据非常大,那么恭喜你,最后你得到的结果是:我已经很努力了,但结果却是个屁!

因此,除非你你的查询结果只有一条记录,否则不要用Map<String, Object> queryForMap(String sql) 来查询数据

对于List<Map<String, Object>> queryForList(String sql),该方法的核心实现如下

public Map<String, Object> mapRow(ResultSet rs, int rowNum) throws SQLException {
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
Map<String, Object> mapOfColValues = createColumnMap(columnCount);
for (int i = 1; i <= columnCount; i++) {
String key = getColumnKey(JdbcUtils.lookupColumnName(rsmd, i));
Object obj = getColumnValue(rs, i);有性能损耗的地方
mapOfColValues.put(key, obj);
}
return mapOfColValues;
}

个人感觉上述方法的实现在性能很接近纯JDBC操作了,先看下getColumnValue(rs, i)的最终实现

/**
......
*/
public static Object getResultSetValue(ResultSet rs, int index) throws SQLException {
Object obj = rs.getObject(index);
String className = null;
if (obj != null) {
className = obj.getClass().getName();
}
if (obj instanceof Blob) {
obj = rs.getBytes(index);
}
else if (obj instanceof Clob) {
obj = rs.getString(index);
}
else if (className != null &&
("oracle.sql.TIMESTAMP".equals(className) ||
"oracle.sql.TIMESTAMPTZ".equals(className))) {
obj = rs.getTimestamp(index);
}
else if (className != null && className.startsWith("oracle.sql.DATE")) {
String metaDataClassName = rs.getMetaData().getColumnClassName(index);
if ("java.sql.Timestamp".equals(metaDataClassName) ||
"oracle.sql.TIMESTAMP".equals(metaDataClassName)) {
obj = rs.getTimestamp(index);
}
else {
obj = rs.getDate(index);
}
}
else if (obj != null && obj instanceof java.sql.Date) {
if ("java.sql.Timestamp".equals(rs.getMetaData().getColumnClassName(index))) {
obj = rs.getTimestamp(index);
}
}
return obj;
}

个人观点:queryForList(String sql)方法性能损耗在于使用了ResultSet接口中的getObject()方法!
Mysql对于该方法的部分实现如下:

/**
......
*/
public Object getObject(int columnIndex) throws SQLException {
checkRowPos();
checkColumnBounds(columnIndex);
int columnIndexMinusOne = columnIndex - 1;
if (this.thisRow.isNull(columnIndexMinusOne)) {
this.wasNullFlag = true;
return null;
}
this.wasNullFlag = false;
Field field;
field = this.fields[columnIndexMinusOne];
switch (field.getSQLType()) {
case Types.BIT:
case Types.BOOLEAN:
if (field.getMysqlType() == MysqlDefs.FIELD_TYPE_BIT
&& !field.isSingleBit()) {
return getBytes(columnIndex);
}

// valueOf would be nicer here, but it isn't
// present in JDK-1.3.1, which is what the CTS
// uses.
return Boolean.valueOf(getBoolean(columnIndex))
                ......
                case Types.DATE:
if (field.getMysqlType() == MysqlDefs.FIELD_TYPE_YEAR
&& !this.connection.getYearIsDateType()) {
return Short.valueOf(getShort(columnIndex));
}
return getDate(columnIndex);
case Types.TIME:
return getTime(columnIndex);
case Types.TIMESTAMP:
return getTimestamp(columnIndex);
           }
可以看到mysql对于该方法的处理是一个一个的类型比较,这个是性能消耗的主要原因。

以下为测试代码
测试数据库mysql
student table 的数据是1013960(在实际生产环境中,如此大数据的查询,我们会采用分页的方式)
long time = System.currentTimeMillis();
String sql = "select id,name,age,time_c,timestamp_c from student ";
new JdbcTemplate(utils.dataSource).queryForList(sql);
System.out.println("consumed time " + ((System.currentTimeMillis() - time)) + " ms");
consumed time 11359 ms

long time = System.currentTimeMillis();
String sql = "select id,name,age,time_c,timestamp_c from student ";
Connection con = utils.dataSource.getConnection();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(sql);
if(rs != null ){
List<Map<String, Object>> list = new ArrayList<Map<String,Object>>();
ResultSetMetaData metaData = rs.getMetaData();
while(rs.next()){
Map<String, Object> map = new HashMap<String, Object>();
map.put(JdbcUtils.lookupColumnName(metaData, 1), rs.getString(1));
map.put(JdbcUtils.lookupColumnName(metaData, 2), rs.getString(2));
map.put(JdbcUtils.lookupColumnName(metaData, 3), rs.getDate(3));
map.put(JdbcUtils.lookupColumnName(metaData, 4), rs.getTime(4));
map.put(JdbcUtils.lookupColumnName(metaData, 5), rs.getTimestamp(5));
list.add(map);
}
}
System.out.println("consumed time " + ((System.currentTimeMillis() - time)) + " ms");
consumed time 8703 ms
//注JdbcUtils来自spring,本文只关心测试结果,相应的资源关闭代码未贴出。

可以看到与纯JDBC的操作相比方法queryForList(String sql)拥有不错的性能开销,再加上new JdbcTemplate(utils.dataSource).queryForList(sql)能取代JDBC那么多样板,这个方法很棒

猜你喜欢

转载自huangcongweng.iteye.com/blog/1847137