Special Cloudscape Programming
Page 3 of 7

Programming Serializable Classes

Cloudscape can store objects that are serializable. This section discusses how to create serializable classes.

For an overview of JavaSoft's serialization mechanisms, see http://www.java.sun.com/products/jdk/1.1/docs/guide/serialization.

This section covers the following topics:

Requirements for Serialization

When you define a column to hold a Java object, you declare that the column serializes a particular class or interface.

Requirements for classes:

  • implement the java.io.Serializable or the java.io.Externalizable interface (classes that implement Externalizable must also have a public no-argument constructor)
  • exist at the time the column is created and accessed
  • be a public class

Requirement for interfaces:

  • extend the java.io.Serializable interface

The java.io.Externalizable interface extends java.io.Serializable, and so classes that extend either interface are actually "serializable." When this documentation refers to serializable classes (with a lowercase s), those classes can extend either Serializable or Externalizable.

Serializable vs. Externalizable

The choice between implementing java.io.Serializable and java.io.Externalizable depends on whether you want ease of use or optimized storage. For classes that implement java.io.Serializable, you do not have to write any methods to ensure that the object is stored properly. Java automatically implements the way the object is written to the output stream (and thus to the Cloudscape database). Define the column and you are finished.

The java.io.Externalizable interface requires more work to implement; if you choose to implement Externalizable, you stop getting the benefits of the automatic implementation. You must correctly implement the writeExternal and readExternal methods so that all the fields of the class and its superclasses are stored. In addition, the class must have a no-argument constructor.

However, the java.io.Serializable implementation requires a certain amount of storage overhead. For classes that implement the java.io.Serializable interface, for each instance of the class, Java writes out:

If you are storing thousands of records, that is a lot of overhead.

Classes that implement java.io.Externalizable require less overhead when stored. For each instance of the class, Java writes out:

  • class metadata

    The class name of the class only (not superclasses)

  • the serialVersionUID
  • data (whatever the writeExternal method stores)

Java does not write out any superclass metadata or any field metadata at all.

Marking Fields as Transient

In addition, when using either java.io.Serializable or java.io.Externalizable, you can design your classes for more compact storage by storing only the fields needed to reconstruct a class. If you are using java.io.Serializable, you can mark a field as transient to indicate that the field does not need to be stored with the rest of the object. The field is automatically omitted from the output and input streams. If you are using java.io.Externalizable, you must write the writeExternal and readExternal methods to omit the writing and reading of the appropriate field. However, it is useful documentation practice to mark the field as transient.

For an example of a transient field, see the JBMSTours.serializabletypes.HotelStay class in the sample database included in the installation.

NOTE: Static fields are not serialized.

Modifying Classes (Java Versioning)

Developers often modify classes after applications have been deployed. When they make minor changes, they want to be able to continue to use the stored instances of the earlier versions of the classes. The serialization mechanisms track all changes to classes and check the compatibility of classes with stored objects. You must explicitly indicate that the class is still compatible when you make any changes to it. Otherwise, the serialization mechanism assumes that the class change is incompatible.

Of course, when the changes you make are major, you may not want to mix instances of the new class with instances of the old class within the database. In this case, you mark the class as a new version from the serialization perspective. You will not be able to access instances of the earlier version of the class. This means that any SQL-J statement that selects the older instances will fail.

Cloudscape does not recognize the new class definition until you restart Cloudscape.

The rest of this section assumes that the changes you make are minor and that you want to keep the new class compatible (from the viewpoint of serialization) with the old class. When you want to change a class definition when a column serializes that type, you must follow Java's rules for modifying the class to keep the different versions of the class compatible. These rules, which are discussed under the general topic of "versioning," are documented at http://www.java.sun.com/products/jdk/1.1/docs/guide/serialization/spec/version.doc.html.

Compatible and Incompatible Changes

Some changes to a class are considered compatible changes, which means that these changes do not affect the interoperability of the class versions. For example, adding a new method to a class is a compatible change. If you have a column that stores objects of that class, and then you redefine the class and restart Cloudscape, you will not have any problems storing instances of the new version of the class or retrieving instances of the old version of the class.

Some changes, however, are incompatible. Incompatible changes void the interoperability of class versions. For example, deleting a field from a class is an incompatible change. Deleting a field from a class, restarting Cloudscape, and then trying to access instances of that class from the database raises a java.io.InvalidClassException. (Because the JDBC interface raises only SQLExceptions, you cannot "catch" this exception. The error is wrapped in an SQLException.) See the Java documentation at http://www.java.sun.com/products/jdk/1.1/docs/guide/serialization/spec/version.doc.html for a complete list of compatible and incompatible changes.

Using the serialVersionUID to Indicate Serializable Compatibility

When you alter a class definition by making a compatible change, you still must identify the class as being compatible with the original version of the class. Java implicitly identifies each unique class with what is called a serialVersionUID, an automatically generated ID, which is stored when objects are serialized. When you modify a class, you can explicitly include the original class's automatically generated serialVersionUID in the altered class definition. You use Java's serialver program to find out a serializable class's serialVersionUID.

NOTE: The Java documents (http://www.java.sun.com/products/jdk/1.1/docs/guide/serialization/spec/class.doc.html) state that you can explicitly declare this serialVersionUID in the original class definition but that it is not required. Cloudscape recommends that you identify a class's version in the original class definition.

You must use the words "static final long" when declaring the variable; otherwise, the serialization mechanisms do not use the value.

Versioning Mini Tutorial

This section is an example of the way using serialVersionUID helps you keep classes compatible.

You define a very simple user class, called UserClass, which does not explicitly define the serialVersionUID.

import java.io.Serializable;
/**
 * This is a sample user class for showing versioning problems.
 */
public class UserClass implements Serializable {
    public int my_id;
    public String name;
    public UserClass () {
        my_id = 1;
        name = "SFO";
        }
    public int getId() {
        return my_id;
    }
    public String getName() {
        return name;
    }
}

You define a table that serializes the class and store some values in it:

CREATE TABLE myTable (the_id INTEGER, the_class
    SERIALIZE(UserClass))

INSERT INTO myTable VALUES (1, new UserClass())

You then shut down Cloudscape and alter the class with a compatible change. However, you neglect to add the serialVersionUID.

import java.io.Serializable;
/**
 * This is a sample user class for showing versioning problems.
 */
public class UserClass implements Serializable {
    public int my_id;
    public String name;
    /*Here is the first change*/
    public int second_id;
    public UserClass () {
        my_id = 1;
        name = "SFO";
    /*Here is the second change*/
        second_id = 2;*/
        }
    }
    public int getId() {
         return my_id;
    }
    public String getName() {
        return name;
    }
    /*Here is the third change*/
    public String getAString () {
        return getId() + " " + getName() + second_id;
    }
}

When you restart Cloudscape, Cloudscape allows you to insert new rows into the table without complaint. However, when you try to retrieve rows from the table, you get a java.io.InvalidClassException (wrapped in an SQLException), even though the changes are compatible:

ERROR XJ001: Java exception: 'UserClass; Local class not compatible: java.io.InvalidClassException'.

Java does not recognize the class as compatible because the automatically generated serialVersionUID in the new version does not match the automatically generated one in the first version.

However, the scenario would be different if you had added the serialVersionUID of the original class (which you can find out by running serialver -show from the command line).

static final long serialVersionUID = -1600135205756904045L;

In this scenario, you can access data from both versions of the class. You can even execute the new method on the objects that belong to the older version.

SELECT the_class.getAString() FROM mytable

The query returns "0" for rows that contain objects from the first version of the class (which did not contain this field).

However, you may still raise java.io.InvalidClassExceptions. Explicitly including the serialVersionUID in the original class definition helps you avoid that problem.

Other Kinds of Versioning

Finally, you may want to implement your own "versioning" of a class for other reasons. That is, you make a significant change but one that is compatible in terms of serialization to the class, but you still want to be able to store instances of the new class alongside instances of the old class in the same table. You do not want to lose your old data. However, the change is significant enough that some method invocation or field access will give odd results if executed on the old members. One way to handle this is to include an identifier such as currentClassId along with a getCurrentClassId method. The method allows you to specify versions when retrieving records from the database.

The following example shows a new class. The changes to this class from the previous version (which is not shown) are:

  • The currentClassVersion has been incremented.
  • The field third_id has been added.
import java.io.Serializable;
/**
 * This is a sample user class showing how you might implement 
class-specific, non-serializable versioning.
 */
public class UserClass implements Serializable {
    public int my_id;
    public String name;
    public int second_id;
/* new field */
    public int third_id;
    static final long serialVersionUID = -1600135205756904045L;
    public static int currentClassVersion;

    public UserClass () {
        my_id = 1;
        name = "SFO";
        second_id = 2;
        third_id = 3;
/* this is a new version of the class. currentClassVersion is
set to 2. In the old version, it was 1).*/
currentClassVersion = 2; } public int getId() { return my_id; }     public String getName() {         return name;     } public String toString () { return getId() + " " + getName() + second_id; }  public int getCurrentClassVersion() { return currentClassVersion; } }

Using the currentClassVersion in the WHERE clause allows you to do a SELECT like the following without any problems:

-- without the WHERE clause, you would get a
-- divide-by-zero error for those rows in which
-- instances of the original version of the class
-- were stored
SELECT the_class->second_id/the_class->third_id
    WHERE the_class.getCurrentClassVersion() = 2

Serializable Objects and Input/Output Streams

The JDBC methods setBytes and setString are capable of sending unlimited amounts of data. Cloudscape supports the JDBC getXXXStream and setXXXStream methods (see java.sql.PreparedStatement and java.sql.ResultSet in the Cloudscape Reference Manual) to pass large blobs of data in smaller chunks (streams). When the statement is executed, the JDBC driver makes repeated calls to this input stream, reading its contents and transmitting those contents as the actual parameter data.

Only the JDBC data types shown in Table 6-1, "Streamable JDBC Data Types" in the Cloudscape Reference Manual (and their corresponding SQL-J types) support streams. You cannot use streams to store user-defined data types in columns of the user-defined data type. However, user-defined types that properly implement writeObject(java.io.ObjectOutputStream) and readObject(java.io.ObjectInputStream) can use ObjectInputStreams and can be stored in columns of the supported data types.