SourceForge.net

Immutable Object Tutorial

Author: Renato Brunella

This tutorial demonstrates how immutable objects can be mapped. Immutable objects get their state during initialzation through parameters in the constructor. The example is an exchange rate calculator that reads the exchange rates from the database.

Tutorial Files

You can find the source code in the Code Repository in the sf.qof.tutorial.immutable package.

ExchangeRateCalculator.java    This file contains the main command line control loop.
Currency.java An immutable currency object.
CurrencyCode.java A currency code enumeration (USD, GBP, etc).
DatabaseSetup.java Database setup.

Running the Tutorial

Make sure you built the tutorial jar using Ant.

Use

java -cp dist/qof-tutorial-1.0.0.jar;lib/qof-1.0.0.jar;lib/cglib-2.1_3.jar;lib/asm-1.5.3.jar;lib/hsqldb.jar sf.qof.tutorial.immutable.ExchangeRateCalculator

to run the example (replace ; with : on Unix based systems).

Alternatively under Windows you can use the run.bat batch file:

run.bat sf.qof.tutorial.immutable.ExchangeRateCalculator

Source Code Walkthrough

The Currency class is immutable i.e. once it is initialized with a state this state cannot be changed anymore. Therefore it cannot have any setter methods and the state must be passed in through the constructor:

In the Introduction we saw an example of how to map database columns to fields in objects using setter methods and here we will show how the mapping to parameters in a constructor work. Further we will show how to use custom mapping adapters to map enumerations.

The ExchangeRateCalculator class has a main method that creates a new instance of ExchangeRateCalculator and calls run() displaying all supported currencies and waits for some user input.

The constructor of ExchangeRateCalculator first sets up the database with default values then registers a custom mapping adapter and initializes the currency cache with the values from the database:

  private ExchangeRateCalculator() throws SQLException {
    DatabaseSetup.setup();
    ExchangeRateDAO dao = 
	  QueryObjectFactory.createQueryObject(ExchangeRateDAO.class);
    Connection connection = DataSourceFactory.getDataSource().getConnection();
    dao.setConnection(connection);
    currencyCache = dao.retrieveExchangeRates();
    connection.close();
  }
  
  private Map<CurrencyCode, Currency> currencyCache;

The currecy cache is defined as a map with a key of CurrencyCode enumeration and value of Currency.

Let's have a look at ExchangeRateDAO the query interface definition to see how we can initialize the currency cache:

  private interface ExchangeRateDAO extends BaseQuery {
    @Query(sql = "select currency_code {enum%%1,enum%%*}, " +
	  "exchange_rate {double%%2} from exchange_rate")
    Map<CurrencyCode, Currency> retrieveExchangeRates() throws SQLException;
  }

The SQL select statement simply selects two columns currency_code and exchange_rate from the exchange_rate table. We want to use the currency_code column as the key for values in the result Map. This is done using two percent characters followed by a star and because enumeration mapping uses a custom mapping adapter we need to specify the type "enum": {enum%%*}.

The value in the map is of type Currency and we need to use the constructor:

  public Currency(CurrencyCode currencyCode, double amount)

It takes two parameters of type CurrencyCode and double. To map to a parameter in the constructor you need to define a result mapping with type followed by two percent charaters and followed by the parameter index: {enum%%1} and {double%%2}. The type must to be defined to enable QueryObjectFactory to select the correct constructor.

QueryObjectFactory will try to find a matching constructor in Currency that has two parameters of type Enumeration and double. If no matching constructor can be found an exception is thrown.

The result set column currency_code is mapped to both the key in the map and the first parameter in the constructor of Currency. This is done using {enum%%1,enum%%*}.

To enable mapping of enumerations we use the EnumMappingGenerator custom mapping adapter. This adapter is pre-registered.

The EnumMappingGenerator custom mapping adapter uses the valueOf(String) and name() methods of an enumeration to map to a string database column.

Registration of custom mapping adapter applies to all code generation in a application. To unregister an adapter use QueryObjectFactory.registerMapper(String typeName).