批改dbunit的几个bug兼对dbunit进行扩展
修改dbunit的几个bug兼对dbunit进行扩展
最近在对unitils进行扩展, 主要是数据库这块儿的内容, 对, 就是dbunit, dbunit给我的感觉是扩展性太差了, 很多类的构造函数采用包可见, 抽象类的抽象方法包可见, 根本没办法继承复写某些方法, 可定制性和unitils比起来也差的不是一点点, 根本就是一个封闭的系统. 导致很多代码不得不大段的拷贝代码来满足自身的需要.
我这里采用了excel格式来构造测试数据, 目前发现的dbunit(使用版本为2.4.6)的几个问题:
针对以上问题的解决方案:
科学计数法问题
这里需要对XlsTable中的getValue()方法下进行处理, 具体代码如下:
这里暂定精确到小数5位, 有没有更好的解决方案?
日期时间问题
在某个地方对TimeZone做个初始化, 具体原理懒得去探究了, 反正我这样解决了问题
空行的问题
这个需要实现一个IBatchStatement, 在批量处理数据的时候, 如果遇到空行应该跳过, dbunit有一个自己的实现:BatchStatement, 日!把构造函数声明为包可见, 让你无从继承改写. 无奈copy一些代码重写了一个. 另外还需要重写一个PreparedStatementFactory, 用来创建自己的那个IBatchStatement实现类.
具体就是加了几个判断, 批量处理的行数据为null的时候不进行批量操作
其实dbunit也有一个类似unitils的控制中心:DatabaseConfig类. 但是可配置的东东太少太少, 我需要的没有, 有的我一个都不需要:(, 无语.
根据唯一键清数据
这个借鉴了dbunit原来的做法(现在去掉了唯一键标识的功能), 给字段加下划线style来标识. 本来打算根据数据库的metadata信息来获取唯一键的, 但是jdbc没有提供这样的接口, 而这种用下划线标识具有更好的扩展性, 可维护性, 可移植性.
具体做法是在XlsTable的createMetaData()方法中检查字段的style, 然后利用了Column中的remark属性来存储唯一键标识信息.
然后重新定义了一个DatabaseOperation:
dbunit中很多代码没法重用, 再一次祭出copy大法.
通过上面的步骤基本可以让unitils+dbunit能够满足目前项目的测试需要了.
最近在对unitils进行扩展, 主要是数据库这块儿的内容, 对, 就是dbunit, dbunit给我的感觉是扩展性太差了, 很多类的构造函数采用包可见, 抽象类的抽象方法包可见, 根本没办法继承复写某些方法, 可定制性和unitils比起来也差的不是一点点, 根本就是一个封闭的系统. 导致很多代码不得不大段的拷贝代码来满足自身的需要.
我这里采用了excel格式来构造测试数据, 目前发现的dbunit(使用版本为2.4.6)的几个问题:
- 有些大数字会采用科学计数法来表示, 导致在解析的时候数据不正确
- dbunit内部对日期时间的处理会用到TimeZone这个东东, 这个在国际化方面应该是有价值的, 但是在我们的测试中却会导致时间与格林威治时间做8个小时的偏移转换
- 对excel中的空行没有进行处理(比如excel本来只有两行数据, 但是不知什么原因会存在一些空行, 导致在插入数据库的时候会有Null相关的错误)
- 对测试数据不仅涉及到根据主键清理, 有时候还需要根据一些特殊的字段值进行清理, 比如唯一键, 而这个需要进行扩展, 而XlsTable的包可见, 基本上必须另外实现一套(TMD,恨得让人直咬牙).
针对以上问题的解决方案:
科学计数法问题
这里需要对XlsTable中的getValue()方法下进行处理, 具体代码如下:
static final Pattern pattern = Pattern.compile("[eE]"); private BigDecimal toBigDecimal(double cellValue) { String resultString = String.valueOf(cellValue); // 针对科学计数法的处理(对于小数位数精确到5位) if (pattern.matcher(resultString).find()) { DecimalFormat format = new DecimalFormat("#####.#####"); resultString = format.format(cellValue); } if (resultString.endsWith(".0")) { resultString = resultString.substring(0, resultString.length() - 2); } BigDecimal result = new BigDecimal(resultString); return result; }
这里暂定精确到小数5位, 有没有更好的解决方案?
日期时间问题
在某个地方对TimeZone做个初始化, 具体原理懒得去探究了, 反正我这样解决了问题
// 由于dbunit对excel的时间处理会使用TimeZone.getOffset()做一个偏移转换, 这里需要设置一下 TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
空行的问题
这个需要实现一个IBatchStatement, 在批量处理数据的时候, 如果遇到空行应该跳过, dbunit有一个自己的实现:BatchStatement, 日!把构造函数声明为包可见, 让你无从继承改写. 无奈copy一些代码重写了一个. 另外还需要重写一个PreparedStatementFactory, 用来创建自己的那个IBatchStatement实现类.
public class TPreparedBatchStatement implements IPreparedBatchStatement { boolean notAllNull; // 所有参数是否为null boolean noParameter = true; // 是否指定参数 private int _index; ... public void close() throws SQLException { ... } public void addValue(Object value, DataType dataType) throws TypeCastException, SQLException { logger.debug("addValue(value={}, dataType={}) - start", value, dataType); noParameter = false; // Special NULL handling if (value == null || value == ITable.NO_VALUE) { _statement.setNull(++_index, dataType.getSqlType()); return; } notAllNull = true; dataType.setSqlValue(value, ++_index, _statement); } public void addBatch() throws SQLException { logger.debug("addBatch() - start"); // 没有参数, 或者有参数, 但是参数不全为null if (noParameter || (!noParameter && notAllNull)) { _statement.addBatch(); notAllNull = false; noParameter = true; } _index = 0; } ...
具体就是加了几个判断, 批量处理的行数据为null的时候不进行批量操作
其实dbunit也有一个类似unitils的控制中心:DatabaseConfig类. 但是可配置的东东太少太少, 我需要的没有, 有的我一个都不需要:(, 无语.
根据唯一键清数据
这个借鉴了dbunit原来的做法(现在去掉了唯一键标识的功能), 给字段加下划线style来标识. 本来打算根据数据库的metadata信息来获取唯一键的, 但是jdbc没有提供这样的接口, 而这种用下划线标识具有更好的扩展性, 可维护性, 可移植性.
具体做法是在XlsTable的createMetaData()方法中检查字段的style, 然后利用了Column中的remark属性来存储唯一键标识信息.
Column column = null; // 标识唯一键 byte underline = cell.getCellStyle().getFont(workbook).getUnderline(); if (underline == 1) { column = new Column(columnName, DataType.UNKNOWN, null, null, null, "unique", null); } else { column = new Column(columnName, DataType.UNKNOWN); } columnList.add(column);
然后重新定义了一个DatabaseOperation:
public class DeleteByUniqueKeyOperation extends DatabaseOperation { private static final Logger logger = LoggerFactory.getLogger(DeleteByUniqueKeyOperation.class); @Override public void execute(IDatabaseConnection connection, IDataSet dataSet) throws DatabaseUnitException, SQLException { logger.debug("execute(connection={}, dataSet={}) - start", connection, dataSet); IStatementFactory factory = getFactory(connection); // for each table ITableIterator iterator = dataSet.iterator(); while (iterator.next()) { ITable table = iterator.getTable(); // Do not process empty table if (isEmpty(table)) { continue; } List<String> uniqueKeys = getUniqueKeys(connection, table); if (uniqueKeys == null || uniqueKeys.size() == 0) { continue; } String sql = getSql(table, connection, uniqueKeys); executeSql(connection, factory, table, sql, uniqueKeys); } } private IStatementFactory getFactory(IDatabaseConnection connection) { DatabaseConfig databaseConfig = connection.getConfig(); IStatementFactory factory = (IStatementFactory) databaseConfig.getProperty(DatabaseConfig.PROPERTY_STATEMENT_FACTORY); return factory; } private void executeSql(IDatabaseConnection connection, IStatementFactory factory, ITable table, String sql, List<String> uniqueKeys) throws DatabaseUnitException, SQLException { IPreparedBatchStatement statement = null; ITableMetaData metaData = getOperationMetaData(connection, table.getTableMetaData()); Column[] columns = metaData.getColumns(); int count = table.getRowCount(); try { for (int i = 0; i < count; i++) { if (statement != null) { statement.executeBatch(); statement.clearBatch(); statement.close(); } statement = factory.createPreparedBatchStatement( sql, connection); addBatchValue(table, statement, columns, uniqueKeys, i); } statement.executeBatch(); statement.clearBatch(); } finally { if (statement != null) { statement.close(); } } } private void addBatchValue(ITable table, IPreparedBatchStatement statement, Column[] columns, List<String> uniqueKeys, int row) throws SQLException, DataSetException, TypeCastException { for (Column column : columns) { // 必须保证都大写 String columnName = column.getColumnName(); if (!uniqueKeys.contains(columnName)) { continue; } try { statement.addValue(table.getValue(row, columnName), column.getDataType()); } catch (TypeCastException e) { throw new TypeCastException("Error casting value for table '" + table.getTableMetaData().getTableName() + "' and column '" + columnName + "'", e); } } statement.addBatch(); } private String getSql(ITable table, IDatabaseConnection connection, List<String> uniqueKeys) throws SQLException { // 获取唯一键 String tableName = table.getTableMetaData().getTableName(); StringBuilder sql = new StringBuilder(); sql.append("delete ").append(tableName).append(" where "); boolean first = true; for (String uniqueKey : uniqueKeys) { if (first) { first = false; } else { sql.append(" and "); } sql.append(uniqueKey).append("=").append("? "); } return sql.toString(); } private List<String> getUniqueKeys(IDatabaseConnection connection, ITable table) throws SQLException, DataSetException { List<String> uniqueKeys = new ArrayList<String>(); // 一种实现方式 Column[] columns = table.getTableMetaData().getColumns(); for (Column column : columns) { if ("unique".equals(column.getRemarks())) { uniqueKeys.add(column.getColumnName()); } } return uniqueKeys; } static boolean isEmpty(ITable table) throws DataSetException { ... } static ITableMetaData getOperationMetaData(IDatabaseConnection connection, ITableMetaData metaData) throws DatabaseUnitException, SQLException { ... } }
dbunit中很多代码没法重用, 再一次祭出copy大法.
通过上面的步骤基本可以让unitils+dbunit能够满足目前项目的测试需要了.