dbunit经典的NoSuchColumnException解决之道

抱怨来了

dbunit这么多人用,这个项目居然好像没有人在维护了,自动2012年9月release一个版本后,再没有更新了,寒心啊。

dbunit有一个大大的BUG,即会解释不了MySQL表的结构,在使用@DataSet准备数据时,会抛出类似如下的异常:

Caused by: org.unitils.core.UnitilsException: Error while executing DataSetLoadStrategy
    at org.unitils.dbunit.datasetloadstrategy.impl.BaseDataSetLoadStrategy.execute(BaseDataSetLoadStrategy.java:46)
    at org.unitils.dbunit.DbUnitModule.insertDataSet(DbUnitModule.java:230)
    at org.unitils.dbunit.DbUnitModule.insertDataSet(DbUnitModule.java:153)
    ... 35 more
Caused by: org.dbunit.dataset.NoSuchColumnException: t_upload_file.ID -  (Non-uppercase input column: id) in ColumnNameToIndexes cache map. Note that the map's column names are NOT case sensitive.
    at org.dbunit.dataset.AbstractTableMetaData.getColumnIndex(AbstractTableMetaData.java:117)
    at org.dbunit.operation.AbstractOperation.getOperationMetaData(AbstractOperation.java:89)
    at org.dbunit.operation.AbstractBatchOperation.execute(AbstractBatchOperation.java:140)
    at org.dbunit.operation.CompositeOperation.execute(CompositeOperation.java:79)
    at org.unitils.dbunit.datasetloadstrategy.impl.CleanInsertLoadStrategy.doExecute(CleanInsertLoadStrategy.java:45)
    at org.unitils.dbunit.datasetloadstrategy.impl.BaseDataSetLoadStrategy.execute(BaseDataSetLoadStrategy.java:44)
    ... 37 more

网上有很多痛苦的人在苦苦寻答案,但都依旧痛苦着...
http://zfanxu.iteye.com/blog/1508339
http://zfanxu.iteye.com/blog/1508339

其实这是dbunit的一个BUG,好像很多版本都有这个问题,报告说解决了,其实并没有解决。我使用最新的2.4.9的版本照样会抛出这个问题。

解决办法

碰到问题光抱怨是没有用的,又不能指望dbunit的作者改,只能自己着腾了。按照网上的几篇文章改了dbunit的源码,重新编译上传到自己的Maven私服上。终于解决了。

为了避免大家再重新更改编译,我把已经解译好的dbunit jar放在附件中,大家需要的话可以下载使用。

继续...

最近又在整基于DB2的unitils框架,发现又出现问题了,结果再次好好跟踪了unitils及dbunit的源码,终于有了颠覆性的重大发现:

原来网上一直说的是DBUNIT框架导致这个问题的说明是错误的,真正的错误是unitils框架的错误!!

因为DBUNIT已经为不同数据库提供了不同的接口实现:

org.dbunit.database.IMetadataHandler
而unitils(具体地说是DbUnitModule模块)不管你什么数据库,它统一使用这个类:
org.dbunit.database.DefaultMetadataHandler

如果数据库不特殊,当然用DefaultMetadataHandler这个没有问题,如果特殊,则就取不到数据库的Metadata信息了,结果异常就发生了。

但是,目前的DBUnit的Db2MetadataHandler确实是有BUG的,所以我的解决方法是:

1)复写了unitils的DbUnitModule实现类;
2)复写了dbunit的Db2MetadataHandler实现类;
3)配置unitils的配置文件,应用这些自定义的实现类。

package com.ridge.test.unitils.ext;

import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DefaultMetadataHandler;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.datatype.IDataTypeFactory;
import org.dbunit.dataset.filter.ITableFilterSimple;
import org.dbunit.ext.db2.Db2DataTypeFactory;
import org.dbunit.ext.db2.Db2MetadataHandler;
import org.dbunit.ext.mysql.MySqlDataTypeFactory;
import org.dbunit.ext.mysql.MySqlMetadataHandler;
import org.unitils.core.UnitilsException;
import org.unitils.core.dbsupport.DbSupport;
import org.unitils.core.dbsupport.DefaultSQLHandler;
import org.unitils.core.dbsupport.SQLHandler;
import org.unitils.dbunit.DbUnitModule;
import org.unitils.dbunit.util.DbUnitDatabaseConnection;

import javax.sql.DataSource;

import static org.dbunit.database.DatabaseConfig.FEATURE_BATCHED_STATEMENTS;
import static org.dbunit.database.DatabaseConfig.PROPERTY_DATATYPE_FACTORY;
import static org.dbunit.database.DatabaseConfig.PROPERTY_ESCAPE_PATTERN;
import static org.unitils.core.dbsupport.DbSupportFactory.getDbSupport;
import static org.unitils.core.util.ConfigUtils.getInstanceOf;

/**
 * @author : chenxh(quickselect@163.com)
 * @date: 13-10-9
 */
public class MyDbunitModule extends DbUnitModule {

    protected DbUnitDatabaseConnection createDbUnitConnection(String schemaName) {
        // A DbSupport instance is fetched in order to get the schema name in correct case
        DataSource dataSource = getDatabaseModule().getDataSourceAndActivateTransactionIfNeeded();
        SQLHandler sqlHandler = new DefaultSQLHandler(dataSource);
        DbSupport dbSupport = getDbSupport(configuration, sqlHandler, schemaName);

        // Create connection
        DbUnitDatabaseConnection connection = new DbUnitDatabaseConnection(dataSource, dbSupport.getSchemaName());
        DatabaseConfig config = connection.getConfig();

        // Make sure that dbunit's correct IDataTypeFactory, that handles dbms specific data type issues, is used
        IDataTypeFactory dataTypeFactory = getInstanceOf(IDataTypeFactory.class, configuration, dbSupport.getDatabaseDialect());
        config.setProperty(PROPERTY_DATATYPE_FACTORY, dataTypeFactory);
        // Make sure that table and column names are escaped using the dbms-specific identifier quote string
        if (dbSupport.getIdentifierQuoteString() != null)
            config.setProperty(PROPERTY_ESCAPE_PATTERN, dbSupport.getIdentifierQuoteString() + '?' + dbSupport.getIdentifierQuoteString());
        // Make sure that batched statements are used to insert the data into the database
        config.setProperty(FEATURE_BATCHED_STATEMENTS, "true");
        // Make sure that Oracle's recycled tables (BIN$) are ignored (value is used to ensure dbunit-2.2 compliancy)
        config.setProperty("http://www.dbunit.org/features/skipOracleRecycleBinTables", "true");

        //注意这儿:根据不同的数据库(unitils的database.dialect配置参数)为dbunit
        //指定使用不同的IMetadataHandler实现(其它数据库都可以用默认的,还有一个Netezza也是特别的,这里忽略了)
        if("db2".equalsIgnoreCase(configuration.getProperty("database.dialect"))){
            config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY,
                    new Db2DataTypeFactory());
            
            //由于dbunit自身提供的Db2MetadataHandler有BUG,所以这里使用自己写的
            //MyDb2MetadataHandler,源码在后面了。
            config.setProperty(DatabaseConfig.PROPERTY_METADATA_HANDLER,
                    new MyDb2MetadataHandler());
        }else if("mysql".equalsIgnoreCase(configuration.getProperty("database.dialect"))){
            config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY,
                    new MySqlDataTypeFactory());
            config.setProperty(DatabaseConfig.PROPERTY_METADATA_HANDLER,
                    new MySqlMetadataHandler());
        }
        return connection;
    }
}

下面是MyDb2MetadataHandler的源码:

package com.ridge.test.unitils.ext;

import org.dbunit.ext.db2.Db2MetadataHandler;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.dbunit.util.SQLHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author : chenxh(quickselect@163.com)
 * @date: 13-10-9
 */
public class MyDb2MetadataHandler extends Db2MetadataHandler {
    private static final Logger logger = LoggerFactory.getLogger(MyDb2MetadataHandler.class);

    public ResultSet getTables(DatabaseMetaData metaData, String schemaName, String[] tableType)
            throws SQLException
    {
        if(logger.isTraceEnabled())
            logger.trace("tableExists(metaData={}, schemaName={}, tableType={}) - start",
                    new Object[] {metaData, schemaName, tableType} );
        return metaData.getTables(null, schemaName, "%", tableType);
    }

    public boolean tableExists(DatabaseMetaData metaData, String schema, String tableName)
            throws SQLException
    {
        ResultSet tableRs = metaData.getTables(null, schema, tableName, null);
        try
        {
            return tableRs.next();
        }
        finally
        {
            SQLHelper.close(tableRs);
        }
    }

    public ResultSet getColumns(DatabaseMetaData databaseMetaData, String schemaName, String tableName)
            throws SQLException {
        // Note that MySQL uses the catalogName instead of the schemaName, so
        // pass in the given schema name as catalog name (first argument).

        ResultSet resultSet = databaseMetaData.getColumns(
                null, schemaName, tableName, "%");
        return resultSet;
    }

    public boolean matches(ResultSet columnsResultSet, String catalog,
                           String schema, String table, String column,
                           boolean caseSensitive) throws SQLException
    {
        String catalogName = columnsResultSet.getString(1);
        String schemaName = columnsResultSet.getString(2);
        String tableName = columnsResultSet.getString(3);
        String columnName = columnsResultSet.getString(4);

        // MYSQL provides only a catalog but no schema
        if(schema != null && schemaName == null && catalog==null && catalogName != null){
            logger.debug("Switching catalog/schema because the are mutually null");
            schemaName = catalogName;
            catalogName = null;
        }

        boolean areEqual =
                areEqualIgnoreNull(table, tableName, caseSensitive) &&
                        areEqualIgnoreNull(column, columnName, caseSensitive);
        return areEqual;
    }

    private boolean areEqualIgnoreNull(String value1, String value2,
                                       boolean caseSensitive) {
        return SQLHelper.areEqualIgnoreNull(value1, value2, caseSensitive);
    }
}

最后一步,更改unitils.properties的配置:

...
unitils.module.dbunit.className=com.ridge.test.unitils.ext.MyDbunitModule
...

总结一下

采用前面的解决方案只能解决mysql的问题,且直接改dbunit的源码,是不好的方案,现在我把它废弃了,大家就不要了。

采用第二种方案吧,是优雅的解决方案,没有更改dbunit的源码,仅通过unitils的扩展配置实现了,所以你不要下载附件的dbunit-2.4.8.2.jar了,直接使用最新的dbunit版本吧:

        <dependency>
            <groupId>org.dbunit</groupId>
            <artifactId>dbunit</artifactId>
            <version>2.4.9</version>>
            </exclusions>
        </dependency>

这个问题啊,让我死几回的心都有了,现在终于解决了,希望对大家有帮助!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352