Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve internal design of DataSetBuilder to enhance maintainability #2

Closed
rafaelvanderlei opened this issue May 27, 2016 · 3 comments

Comments

@rafaelvanderlei
Copy link
Owner

rafaelvanderlei commented May 27, 2016

org.dbunit.dataset.streamIDataSetConsumer works best when the source is a document that is being parsed while reading in streams, like XML.

To build a IDataSet from objects created purely in Java, a better approach should be to design a domain model with objects that are easy to create so that they can be used by the DataSetBuilder in order to build the IDataSet.

Basically, IDataSet requires the implementation to store a collection of ITable's along with their Metadata (interface ITableMetaData) and the data (composed by rows with their respective columns), so that it can provide information required by IDataSet interface.

Basically, ITableMetaData requires the implementation to provide information about the table, like its name and its columns metadata.

So we need a builder for ITableMetaData and a builder for a collection of tables with rows and columns.

@rafaelvanderlei
Copy link
Owner Author

rafaelvanderlei commented May 27, 2016

Starting from the basics, the domain model, I added the following classes (using pseuco-code to clarify):

A class for Row.

class Row {
//field
ColumnKeyMap<Object> columns;

//methods
Row add(String columnName, Object value);
<T> Row add(ColumnSpec<T> columnSpec, T value);
Object get(String columnName);
Set<String> getColumnNames();
boolean containsColumn(String columnName);
Row copy();
}

A class for Table.

class Table extends org.dbunit.dataset.AbstractTable implements ITableManipulator {
//fields
List<Row> rows = new ArrayList<Row>();
TableMetaDataBuilder tableMetaDataBuilder;
CustomTableMetaData tableMetaData;   
boolean committed = false;

//methods declared by org.dbunit.dataset.ITable
CustomTableMetaData getTableMetaData();
getRowCount();
Object getValue(int row, String column);

//methods declared by ITableManipulator (which is also a new interface)
Table add(Row row);
Table commit();
boolean isCommitted();

//methods declared by this class itself
String getTableName();
Table addValueGenerator( String columnName, ValueGenerator<? extends Object> valueGenerator );
}

A class for Table Metadata.

//Couldn't find a better name.
//Ideally ITableMetaData itself should already provide support for ValueGenerator's.
class CustomTableMetaData extends org.dbunit.dataset.DefaultTableMetaData {

//field
ColumnKeyMap<ValueGenerator<? extends Object>> valueGenerators;

//methods
ValueGenerator<? extends Object> getValueGenerator( String columnName );
void applyValueGenerators( Row...rows );
}

As we can see, the classes Table and Row were designed to be easy to be built, similar to the builder pattern fashion. So, there's little need, if any, to write specific builders to construct objects of these classes (although we'll see a counterexample for row in a bit).

However, CustomTableMetaData extends DefaultTableMetaData and it is quite complex to be created, so we need a specific builder in order to create such objects.

We will also need a builder for the DataSet itself and another builder for rows, specifically designed to build rows in such a way that makes it easy to build the dataset working directly with rows (rather than only allowing to build from pre-built tables), so that we can simplify the usage of the dataset builder.

I'll leave the builders to the next comment.

@rafaelvanderlei
Copy link
Owner Author

rafaelvanderlei commented May 27, 2016

In order to make it easy to join all the pieces together, I added/changed the following builder classes:

TableMetaDataBuilder (changed the already existing class)

class TableMetaDataBuilder {
//fields
String tableName;
ColumnKeyMap<Column> keysToColumns = new ColumnKeyMap<Column>();
ColumnKeyMap<ValueGenerator<? extends Object>> valueGenerators = new ColumnKeyMap<ValueGenerator<? extends Object>>();

//methods
TableMetaDataBuilder with(ITableMetaData metaData) throws DataSetException;
TableMetaDataBuilder with(Column... columns);
TableMetaDataBuilder with(Column column);
TableMetaDataBuilder with(String column);
TableMetaDataBuilder addValueGenerator( String columnName, ValueGenerator<? extends Object> valueGenerator );
CustomTableMetaData build();
String getTableName();

DataSetBuilder (changed the already existing class)

class DataSetBuilder {
//fields
List<IDataSet> additionalDataSets = new ArrayList<IDataSet>();
List<Table> tables = new ArrayList<Table>();    
Map<String,Table> tablesByName = new HashMap<String, Table>();
boolean caseSensitiveTableNames;
IStringPolicy stringPolicy;

//methods
DataSetBuilder add(Table table);
DataSetBuilder add( AbstractDataSetRowBuilder rowBuilder ); //perhaps the most convenient method to use.
DataSetBuilder add( IDataSet newDataSet );
IDataSet build() throws DataSetException;
}

AbstractDataSetRowBuilder

  • added, along with RawDataSetRowBuilder, to replace previous existing classes BasicDataRowBuilder and DataRowBuilder
  • this is the class that all custom RowBuilders must inherit from
abstract class AbstractDataSetRowBuilder {
//field
Row row;

//methods
String getTableName();
Row build();
protected abstract CustomTableMetaData getTableMetaData();
protected <T extends AbstractDataSetRowBuilder> T _with( String columnName, Object value )
}

RawDataSetRowBuilder

  • useful to quickly create raw rows ready to be added to dataset
  • the drawbacks, comparing to custom table-specific implementations of AbstractDataSetRowBuilder, are that the column names must be informed and there won't be support for type-safety
class RawDataSetRowBuilder extends AbstractDataSetRowBuilder {
//field
TableMetaDataBuilder tableMetaDataBuilder;

//methods
static RawDataSetRowBuilder newRawRow( String tableName );
<T extends RawDataSetRowBuilder> T with( String columnName, Object value );
<T extends RawDataSetRowBuilder, E> T with( ColumnSpec<E> columnSpec, E value );
}

I'll leave the usage of the builders to the next comment.

@rafaelvanderlei
Copy link
Owner Author

All of the work previously showed was done so that we can build IDataSet's like this:

  • RawDataSetRowBuilder - the simplest usage of a DataSetBuilder (although not the most powerful).
IDataSet dataSet = 
    new DataSetBuilder()
    .add( newRawRow("PERSON").with("NAME", "Bob").with("AGE", 18) )
    .add( newRawRow("ADDRESS").with("STREET", "Main Street").with("NUMBER", 42) )
    .build();
  • Custom table-specific AbstractDataSetRowBuilder - the most powerful and useful usage of DataSetBuilder

First, we need to create the concrete AbstractDataSetRowBuilder (only once per table):

class PERSONRowBuilder extends AbstractDataSetRowBuilder {

    public static final String TABLE_NAME = "PERSON";

    private static final String C_ID = "ID";
    private static final String C_NAME = "NAME";
    private static final String BIRTHPLACE = "BIRTHPLACE";
    private static final String C_VERSION = "VERSION";

    public static final CustomTableMetaData PERSON_TABLE_META_DATA;

    static {
        PERSON_TABLE_META_DATA = new TableMetaDataBuilder( TABLE_NAME )
                .addValueGenerator( C_ID, newFixedValueGenerator( 0L ) )
                .addValueGenerator( C_NAME, newFixedValueGenerator("") )
                .addValueGenerator( C_BIRTHPLACE, newFixedValueGenerator( "unknown" ) )
                .addValueGenerator( C_VERSION, newFixedValueGenerator(1L) )
                .build();
    }

    public static PERSONRowBuilder newPERSON() {
        return new PERSONRowBuilder();
    }

    public final PERSONRowBuilder ID (Long value) {
        return super._with(C_ID, value);
    }

    public final PERSONRowBuilder NAME (String value) {
        return super._with(C_NAME, value);
    }

    public final PERSONRowBuilder BIRTHPLACE (String value) {
        return super._with(C_BIRTHPLACE, value);
    }

    public final PERSONRowBuilder VERSION (Long value) {
        return super._with(C_VERSION, value);
    }

    @Override
    protected CustomTableMetaData getTableMetaData() {
        return PERSON_TABLE_META_DATA;
    }
}

Note that PERSON_TABLE_META_DATA was made static, so there will be only one instance in memory.
Also, note that if a column is not informed, then default generated values will be used.

Then, the actual usage is as simple as this:

IDataSet dataSet = new DataSetBuilder()
    .add( newPERSON().NAME("Bob") )
    .add( newPERSON().NAME("Alice").BIRTHPLACE("London") )
    .build();

And the resulting IDataSet would be similar to this FlatXmlDataSet:

<dataset>
  <PERSON NAME="Bob" ID="0" FIRSTNAME="" BIRTHPLACE="unknown" VERSION="1"/>
  <PERSON NAME="Alice" ID="0" FIRSTNAME="" BIRTHPLACE="London" VERSION="1"/>
</dataset>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant