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:
- Recompilation - If the second version of your application is
compiled on a different platform from the first version, then your serialized
objects may fail to deserialize. This problem and a possible workaround are
discussed in the "Version Control" section near the end of this
Serialization Primer and in the last paragraph of the
header comment for java.io.Serializable.
- Evolution - Your tools for evolving a class which simply implements
java.io.Serializable are very limited.
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.