Plugins : RDBMS Java Types

When persisting a class to an RDBMS datastore there is a mapping process from class/field to table/column. DataNucleus provides a mapping process and allows users to define their own mappings where required. Each field is of a particular type, and where a field is to be persisted as a second-class object you need to define a mapping. DataNucleus defines mappings for all of the required JDO/JPA types but you want to persist some of your own types as second-class objects. This extension is not required for other datastores, just RDBMS.

A type mapping defines the way to convert between an object of the Java type and its datastore representation (namely a column or columns in a datastore table). There are 2 types of Java types that can be mapped. These are mutable (something that can be updated, such as java.util.Date) and immutable (something that is fixed from the point of construction, such as java.awt.Color).

The examples in this guide relate to current DataNucleus SVN. Please consult the DataNucleus source code if you are using an earlier version since things have changed recently for user type mapping



Java type to single column mapping

To map any Java type to a datastore column you need this mapping class we've mentioned. The simplest way to describe how to define your own mapping is to give an example. Lets assume we have our own class IPAddress that represents an address such as 192.168.1.1 and we want to map this to a single VARCHAR column in the datastore

public class IPAddress
{
    String ipAddress;

    public IPAddress(String ipAddr)
    {
        ipAddress = ipAddr;
    }

    public String toString()
    {
        return ipAddress;
    }
}

We start by defining a JavaTypeMapping for this type.

import org.datanucleus.store.mapped.mapping.ObjectAsStringMapping;

public class IPAddressMapping extends ObjectAsStringMapping
{
    private static IPAddress mappingSampleValue = new IPAddress("192.168.1.1");

    public Object getSampleValue(ClassLoaderResolver clr)
    {
        return mappingSampleValue;
    }

    /**
     * Method to return the Java type being represented
     * @return The Java type we represent
     */
    public Class getJavaType()
    {
        return IPAddress.class;
    }

    /**
     * Method to return the default length of this type in the datastore.
     * An IP address can be maximum of 15 characters ("WWW.XXX.YYY.ZZZ")
     * @return The default length
     */
    public int getDefaultLength(int index)
    {
        return 15;
    }

    /**
     * Method to set the datastore string value based on the object value.
     * @param object The object
     * @return The string value to pass to the datastore
     */
    protected String objectToString(Object object)
    {
        String ipaddr;
        if (object instanceof IPAddress)
        {
            ipaddr = ((IPAddress)object).toString();
        }
        else
        {
            ipaddr = (String)object;
        }
        return ipaddr;
    }

    /**
     * Method to extract the objects value from the datastore string value.
     * @param datastoreValue Value obtained from the datastore
     * @return The value of this object (derived from the datastore string value)
     */
    protected Object stringToObject(String datastoreValue)
    {
        return new IPAddress(datastoreValue.trim());
    }
}

We have extended the DataNucleus convenience class ObjectAsStringMapping and this provides the majority of the mapping for us (including JDOQL capabilities). We have defined a default length of 15 so that when an IP Address is to be mapped, if the user doesnt specify a length in the metadata it will be assigned a column of length 15. Finally we define the methods of converting the IPAddress into a String and back again.

The only remaining thing we need to do is enable use of this Java type when running DataNucleus. To do this we create a "plugin.xml" at the root of the CLASSPATH, like this.

<?xml version="1.0"?>
<plugin>
    <extension point="org.datanucleus.store_mapping">
        <mapping java-type="mydomain.IPAddress" mapping-class="mydomain.IPAddressMapping"/>
    </extension>
</plugin>

When using the DataNucleus Enhancer, SchemaTool or Core, DataNucleus automatically searches for the mapping definition at /plugin.xml files in the classpath.

The most common user-type will map to a String and so can just take this as a template. If the user-type maps to a Long then they can use ObjectAsLongMapping - the other convenience mapping available. Note that you also need a MANIFEST.MF as per the Plugins Guide.

This is downloadable as a DataNucleus sample from the Download Page

From this point onwards once you have a field of that type you simply specify the field as this

<field name="myfield" persistence-modifier="persistent"/>

and it will be persisted using your user-type mapping.



Java Type to multiple column mapping

To map any Java type to multiple datastore columns you need also mapping class. The simplest way to describe how to define your own mapping is to give an example. Here we'll use the examle of the Java AWT class Color . This has 3 colour components (red, green, and blue) as well as an alpha component. So here we want to map the Java type java.awt.Color to 4 datastore columns - one for each of the red, green, blue, and alpha components of the colour. To do this we define a mapping class extending the DataNucleus class org.datanucleus.store.mapped.mapping.SingleFieldMultiMapping

package org.mydomain;

import org.datanucleus.PersistenceManager;
import org.datanucleus.metadata.AbstractPropertyMetaData;
import org.datanucleus.store.mapped.DatastoreContainerObject;
import org.datanucleus.store.mapped.mapping.SingleFieldMultiMapping;
import org.datanucleus.store.mapped.mapping.JavaTypeMapping;
import org.datanucleus.store.mapped.query.QueryStatement;
import org.datanucleus.store.mapped.expression.TableExpression;

public class ColorMapping extends SingleFieldMultiMapping
{
    /**
     * Initialize this JavaTypeMapping with the given DatastoreAdapter for
     * the given FieldMetaData.
     *  
     * @param dba The Datastore Adapter that this Mapping should use.
     * @param fmd FieldMetaData for the field to be mapped (if any)
     * @param container The datastore container storing this mapping (if any)
     * @param clr the ClassLoaderResolver
     */
    public void initialize(DatastoreAdapter dba, AbstractMemberMetaData fmd, 
                    DatastoreContainerObject container, ClassLoaderResolver clr)
    {
		super.initialize(dba, fmd, container, clr);

        addDatastoreField(ClassNameConstants.INT); // Red
        addDatastoreField(ClassNameConstants.INT); // Green
        addDatastoreField(ClassNameConstants.INT); // Blue
        addDatastoreField(ClassNameConstants.INT); // Alpha
    }

    public Class getJavaType()
    {
        return Color.class;
    }

    public Object getSampleValue(ClassLoaderResolver clr)
    {
        return java.awt.Color.red;
    }

    public void setObject(PersistenceManager pm, Object preparedStatement, int[] exprIndex, Object value)
    {
        Color color = (Color) value;
        if (color == null)
        {
            getDataStoreMapping(0).setObject(preparedStatement, exprIndex[0], null);
            getDataStoreMapping(1).setObject(preparedStatement, exprIndex[1], null);
            getDataStoreMapping(2).setObject(preparedStatement, exprIndex[2], null);
            getDataStoreMapping(3).setObject(preparedStatement, exprIndex[3], null);
        }
        else
        {
            getDataStoreMapping(0).setInt(preparedStatement,exprIndex[0],color.getRed());
            getDataStoreMapping(1).setInt(preparedStatement,exprIndex[1],color.getGreen());
            getDataStoreMapping(2).setInt(preparedStatement,exprIndex[2],color.getBlue());
            getDataStoreMapping(3).setInt(preparedStatement,exprIndex[3],color.getAlpha());
        }
    }

    public Object getObject(PersistenceManager pm, Object resultSet, int[] exprIndex)
    {
        try
        {
            // Check for null entries
            if (((ResultSet)resultSet).getObject(exprIndex[0]) == null)
            {
                return null;
            }
        }
        catch (Exception e)
        {
            // Do nothing
        }

        int red = getDataStoreMapping(0).getInt(resultSet,exprIndex[0]); 
        int green = getDataStoreMapping(1).getInt(resultSet,exprIndex[1]); 
        int blue = getDataStoreMapping(2).getInt(resultSet,exprIndex[2]); 
        int alpha = getDataStoreMapping(3).getInt(resultSet,exprIndex[3]);
        return new Color(red,green,blue,alpha);
    }

    // --------------------------------- JDOQL Query Methods -------------------------------------------

    public ScalarExpression newLiteral(QueryExpression qs, Object value)
    {
        return null; // Dont support JDOQL querying of Color fields currently
    }

    public ScalarExpression newScalarExpression(QueryExpression qs, LogicSetExpression te)
    {
        return null; // Dont support JDOQL querying of Color fields currently
    }
}

In the initialize() method we've created 4 columns - one for each of the red, green, blue, alpha components of the colour. The argument passed in when constructing these columns is the Java type name of the column data being stored. The other 2 methods of relevance are the setObject() and getObject(). These have the task of mapping between the Color object and its datastore representation (the 4 columns). That's all there is to it.

The above example does not allow the Java type to be used in JDOQL queries. This would involve writing the methods newLiteral(), newScalarExpression() and this detail will be added in a future version of this guide.

The only thing we need to do is enable use of this Java type when running DataNucleus. To do this we create a plugin.xml (at the root of our CLASSPATH) to contain our mappings.

<?xml version="1.0"?>
<plugin>
    <extension point="org.datanucleus.store_mapping">
        <mapping java-type="java.awt.Color" mapping-class="org.mydomain.MyColorMapping"/>
    </extension>
</plugin>

Note that we also require a MANIFEST.MF file as per the Plugins Guide. When using the DataNucleus Enhancer, SchemaTool or Core, DataNucleus automatically searches for the mapping definition at /plugin.xml files in the CLASSPATH.

Obviously, since DataNucleus already supports java.awt.Color there is no need to add this particular mapping to DataNucleus yourself, but this demonstrates the way you should do it for any type you wish to add.

If your Java type that you want to map maps direct to a single column then you would instead extend org.datanucleus.store.mapping.SingleFieldMapping and wouldn't need to add the columns yourself. Look at DataNucleus SVN for many examples of doing it this way.