Programming User-Defined Types

GemFire XD enables you to create user-defined types. A user-defined type is a serializable Java class whose instances are stored in columns.

Note: This topic was adapted from the Apache Derby documentation source, and is subject to the Apache license:
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at
 
         http://www.apache.org/licenses/LICENSE-2.0
 
       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.

User-defined functions can be defined on columns of user-defined types. You can also use these types can as argument types for stored procedures, as described in Programming Data-Aware Procedures and Result Processors.

Note: You cannot register a custom .NET type as a user-defined type in GemFire XD.

The class of a user-defined type must implement the java.io.Serializable interface, and it must be declared to GemFire XD by means of a CREATE TYPE statement. You can install user-defined type classes to GemFire XD as part of a JAR file installation, as described in Storing and Loading JAR Files in GemFire XD.

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.acme.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.acme.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.