批改dbunit的几个bug兼对dbunit进行扩展

修改dbunit的几个bug兼对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能够满足目前项目的测试需要了.