Programming user-defined types

Derby allows you to create user-defined types. A user-defined type is a serializable Java class whose instances are stored in columns. The class must implement the java.io.Serializable interface, and it must be declared to Derby by means of a CREATE TYPE statement.

The key to designing a good user-defined type is to remember that data evolves over time, just like code. A good user-defined type has version information built into it. This allows the user-defined data to upgrade itself as the application changes. For this reason, it is a good idea for a user-defined type to implement java.io.Externalizable and not just java.io.Serializable. Although the SQL standard allows a Java class to implement only java.io.Serializable, this is bad practice for the following reasons:

Fortunately, it is easy to write a version-aware UDT which implements java.io.Serializable and can evolve itself over time. For example, here is the first version of such a class:

package com.example.types;

import java.io.*;
import java.math.*;

public class Price implements Externalizable
{
    // initial version id
    private static final int FIRST_VERSION = 0;

    public String currencyCode;
    public BigDecimal amount;

    // zero-arg constructor needed by Externalizable machinery
    public Price() {}

    public Price( String currencyCode, BigDecimal amount )
    {
        this.currencyCode = currencyCode;
        this.amount = amount;
    }

    // Externalizable implementation
    public void writeExternal(ObjectOutput out) throws IOException
    {
        // first write the version id
        out.writeInt( FIRST_VERSION );

        // now write the state
        out.writeObject( currencyCode );
        out.writeObject( amount );
    }
    
    public void readExternal(ObjectInput in) 
        throws IOException, ClassNotFoundException
    {
        // read the version id
        int oldVersion = in.readInt();
        if ( oldVersion < FIRST_VERSION ) { 
            throw new IOException( "Corrupt data stream." ); 
        }
        if ( oldVersion > FIRST_VERSION ) { 
            throw new IOException( "Can't deserialize from the future." );
        }

        currencyCode = (String) in.readObject();
        amount = (BigDecimal) in.readObject();
    }
}

After this, it is easy to write a second version of the user-defined type which adds a new field. When old versions of Price values are read from the database, they upgrade themselves on the fly. Changes are shown in bold:

package com.example.types;

import java.io.*;
import java.math.*;
import java.sql.*;

public class Price implements Externalizable
{
    // initial version id
    private static final int FIRST_VERSION = 0;
    private static final int TIMESTAMPED_VERSION = FIRST_VERSION + 1;

    private static final Timestamp DEFAULT_TIMESTAMP = new Timestamp( 0L );

    public String currencyCode;
    public BigDecimal amount;
    public Timestamp timeInstant;

    // 0-arg constructor needed by Externalizable machinery
    public Price() {}

    public Price( String currencyCode, BigDecimal amount, 
                  Timestamp timeInstant )
    {
        this.currencyCode = currencyCode;
        this.amount = amount;
        this.timeInstant = timeInstant;
    }

    // Externalizable implementation
    public void writeExternal(ObjectOutput out) throws IOException
    {
        // first write the version id
        out.writeInt( TIMESTAMPED_VERSION );

        // now write the state
        out.writeObject( currencyCode );
        out.writeObject( amount );
        out.writeObject( timeInstant );
    }
      
    public void readExternal(ObjectInput in) 
        throws IOException, ClassNotFoundException
    {
        // read the version id
        int oldVersion = in.readInt();
        if ( oldVersion < FIRST_VERSION ) { 
            throw new IOException( "Corrupt data stream." ); 
        }
        if ( oldVersion > TIMESTAMPED_VERSION ) {
            throw new IOException( "Can't deserialize from the future." ); 
        }

        currencyCode = (String) in.readObject();
        amount = (BigDecimal) in.readObject();

        if ( oldVersion >= TIMESTAMPED_VERSION ) {
            timeInstant = (Timestamp) in.readObject(); 
        }
        else { 
            timeInstant = DEFAULT_TIMESTAMP; 
        }
    }
}

An application needs to keep its code in sync across all tiers. This is true for all Java code which runs both in the client and in the server. This is true for functions and procedures which run in multiple tiers. It is also true for user-defined types which run in multiple tiers. The programmer should code defensively for the case when the client and server are running different versions of the application code. In particular, the programmer should write defensive serialization logic for user-defined types so that the application gracefully handles client/server version mismatches.