Version 2

    This is a variation on UserType for non-default TimeZone with the column being a UserVersionType.  While the changes to the code there are not major, adding the code there would (IMHO) clutter that page too much.

     

    Our application uses a time stamp column for audit purposes. This naturally acts as a version field, but the actual values are generated by a database trigger which also make a copy of the old values and inserts them into an archive table. In the process, it insures that two columns active_on and active_until match up to form continuous record.

     

    For this example, all of that can be omitted, but I give it as background on why we don't just let Hibernate do its thing with a time stamp column. Additionally, our convention is to always store the time stamp in UTC. Having the database manage this makes this a little easier, at least conceptually, since the database creates and manages the timestamps, there is no ambiguity in converting to UTC.

    I'll assume there are (at least) three columns in the table: a primary key generated by the database, some data, and a timestamp that will serve as a version. Here is a mapping file to describe this:

    <?xml version="1.0"?>
    <!DOCTYPE hibernate-mapping PUBLIC
            "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
            "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
    
    <hibernate-mapping>
      <class name="Data">
        <id name="dataId" column="id" type="long" unsaved-value="null">
          <generator class="sequence">
            <param name="sequence">data_id_seq</param>
          </generator>
        </id>
        <version name="activeOn"
                 column="active_on"
                 type="HibernateUTC$TimestampType"
                 generated="always"
                 insert="false"
                 unsaved-value="null"/>
        <property name="description"
                  column="description"
                  type="string"/>
      </class>
    </hibernate-mapping>
    

     

    Adjust your primary key and data columns as necessary.

    This paragraph was in the original and I'm retaining it but it is actually wrong.  The reason is in the following text.

    I have used the above definition (with table names and columns different) with a PostgreSQL database (version 8.3) with only one issue. java.sql.Timestamp values can have sub-millisecond precision (nanoseconds, actually, although YMMV depending on the database). For PostgreSQL the precision seems to be around the microsecond mark as it does for Oracle 10. When using the HiberateUTC$Timestamp type as a UserVersionType, I found it necessary to limit the column precision to milliseconds or else Hibernate incorrectly reports that the row has been updated by another transaction. Somewhere, the sub-millisecond precision is getting lost, whether that be in Hibernate or the PostgreSQL JDBC driver, I can't tell right now. So my table definition for PostgreSQL is

    create table data (
        id serial,
        description varchar(100),
        active_on timestamp(3)
    );

     

    It is considered by some good practice to copy values handed to setters since otherwise you can end up with an extra reference that you didn't work. If you have a time stamp setting like this

    public void setActiveOn(java.sql.Timestamp newVal) {
        activeOn = newVal;
    }
    

     

    The caller can change the value of the timestamp after calling the setter which may not be what you were expecting.  So instead you can do

    public void setActiveOn(java.sql.Timestamp newVal) {
        if (newVal == null) {
            activeOn = null;
        } else {
            activeOn = new Timestamp(newVal.getTime());
        }
    }
    

     

    But that will truncate the timestamp to milliseconds since that's all getTime() returns.  So you want to either call setNanos() explicitly or use clone().

    public void setActiveOn(java.sql.Timestamp newVal) {
        if (newVal == null) {
            activeOn = null;
        } else {
            activeOn = new Timestamp(newVal.getTime());
            activeOn.setNanos(newVal.getNanos());
            // or activeOn = (java.sql.Timestamp) newVal.clone();
        }
    }
    

     

    If you don't, Hibernate will be unable to match the value in the object against the original row in the database and incorrectly conclude that the database row hase been modified by another transaction.

    So here is a table definition for our example:

    create table data (
        id serial,
        description varchar(100),
        active_on timestamp
    );
    

     

    In PostgreSQL, the type serial is implemented by PostgreSQL creating a sequence "under the hood" named data_is_seq which will be used to generate primary key values. If you are curious about the details, see the PostgreSQL documentation. For the purposes of the timestamp/version column, neither the id or description columns are relevant.

     

    The last piece is the modified HibernateUTC taken from UserType for non-default TimeZone. The principle change is to add the pieces necessary for each of the components to implement org.hibernate.usertype.UserVersionType.

     

    I've done one thing that is arguably wrong, which is to give null a sort order in the compare(Object,Object) implementations. Quite frankly, I'm not sure what I should do there, but I think I have at least satisfied the assumption that compare(a,b)=-compare(b,a) even for null cases.

    Another arguable implementation detail is how I initialize HibernateUTC$TimestampType in the seed() and next() methods.  I use System.currentTimeInMillis() rather than using the (commented out) code si.getTimestamp().  I believe that si.getTimestamp() may actually returning nanoseconds which is why I get whacked out distant future dates when I use it (and the column is not generated by the database).

     

    HiberateUTC.java

    import java.io.Serializable;
    import java.sql.Date;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.sql.Timestamp;
    import java.sql.Types;
    import java.util.Calendar;
    import java.util.TimeZone;
    
    import org.hibernate.HibernateException;
    import org.hibernate.cfg.Environment;
    import org.hibernate.engine.SessionImplementor;
    import org.hibernate.usertype.UserType;
    import org.hibernate.usertype.UserVersionType;
    
    
    /**
     * UserType for non-default TimeZone. Hibernate's built-in date, time and timestamp types assume
     * that dates in the database are in Java's default time zone, implicitly. If this assumption is
     * false (and you can't make it true by calling java.util.TimeZone.setDefault), you can configure
     * Hibernate to map to a UserType that does something else....
     * 
     * This code is taken from {@link http ://www.hibernate.org/100.html}. However, there are comments
     * there which apply to us, namely that it would be useful to treat the activeOn property as a
     * version field which is maintained by the database and it is not clear how to get that to work.
     * Also the thread {@link http ://forum.hibernate.org/viewtopic.php?t=980279} suggests handling this
     * conversion in the POJO instead. However, at least one link (reference?) I found indicated a
     * problem when the front-end and the middle layer lived in different time zones. I'm not sure that
     * applies to use since we don't allow the front end to set timestamps however, if London starts
     * handling releases, there may be a problem displaying times in an easily understood form for the
     * users.
     */
    public abstract class HibernateUTC implements UserVersionType, UserType {
    
        /** the SQL type this type manages */
        protected static int[] SQL_TYPES_UTC = { Types.TIMESTAMP };
    
        /**
         * @see net.sf.hibernate.UserType#sqlTypes()
         */
        public int[] sqlTypes() {
            return SQL_TYPES_UTC;
        }
    
        /**
         * @see net.sf.hibernate.UserType#equals(java.lang.Object, java.lang.Object)
         */
        public boolean equals(Object x, Object y) {
            return (x == null) ? (y == null) : x.equals(y);
        }
    
        /**
         * @see net.sf.hibernate.UserType#isMutable()
         */
        public boolean isMutable() {
            return true;
        }
    
        /**
         * @see net.sf.hibernate.UserType#returnedClass()
         */
        public Class<?> returnedClass() {
            return objectClass;
        }
    
        /**
         * The class of objects returned by <code>nullSafeGet</code>. Currently, returned objects are
         * derived from this class, not exactly this class.
         */
        protected Class<?> objectClass = Date.class;
    
        /**
         * Get a hashcode for the instance, consistent with persistence "equality"
         */
        public int hashCode(Object x) throws HibernateException {
            return x.hashCode();
        }
    
        /**
         * Transform the object into its cacheable representation. At the very least this method should
         * perform a deep copy if the type is mutable. That may not be enough for some implementations,
         * however; for example, associations must be cached as identifier values. (optional operation)
         * 
         * @param value the object to be cached
         * @return a cachable representation of the object
         * @throws HibernateException
         */
        public Serializable disassemble(Object value) throws HibernateException {
            return (Serializable) deepCopy(value);
        }
    
        /**
         * Reconstruct an object from the cacheable representation. At the very least this method should
         * perform a deep copy if the type is mutable. (optional operation)
         * 
         * @param cached the object to be cached
         * @param owner the owner of the cached object
         * @return a reconstructed object from the cachable representation
         * @throws HibernateException
         */
        public Object assemble(Serializable cached, Object owner) throws HibernateException {
            return deepCopy(cached);
        }
    
        /**
         * During merge, replace the existing (target) value in the entity we are merging to with a new
         * (original) value from the detached entity we are merging. For immutable objects, or null
         * values, it is safe to simply return the first parameter. For mutable objects, it is safe to
         * return a copy of the first parameter. For objects with component values, it might make sense
         * to recursively replace component values.
         * 
         * @param original the value from the detached entity being merged
         * @param target the value in the managed entity
         * @return the value to be merged
         */
        public Object replace(Object original, Object target, Object owner) throws HibernateException {
            return deepCopy(original);
        }
        
        public Object seed(SessionImplementor si) {
            return null;
        }
        
        public Object next(Object current, SessionImplementor si) {
            return null;
        }
    
        /**
         * Like a Hibernate date, but using the UTC TimeZone (not the default TimeZone).
         */
        public static class DateType extends HibernateUTC {
            protected static int[] SQL_TYPES_DATE = { Types.DATE };
    
            /**
             * @see net.sf.hibernate.UserType#sqlTypes()
             */
            public int[] sqlTypes() {
                return SQL_TYPES_DATE;
            }
    
            /**
             * @see net.sf.hibernate.UserType#deepCopy(java.lang.Object)
             */
            public Object deepCopy(Object value) {
                return (value == null) ? null : new java.sql.Date(((Date) value).getTime());
    
            }
    
            /**
             * @see net.sf.hibernate.UserType#nullSafeGet(java.sql.ResultSet, java.lang.String[],
             *      java.lang.Object)
             */
            public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws SQLException {
                Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
                return rs.getDate(names[0], utcCalendar);
            }
    
            /**
             * @see net.sf.hibernate.UserType#nullSafeSet(java.sql.PreparedStatement, java.lang.Object,
             *      int)
             */
            public void nullSafeSet(PreparedStatement st, Object value, int index) throws SQLException {
                if (!(value instanceof java.sql.Date))
                    value = deepCopy(value);
                Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
                st.setDate(index, (java.sql.Date) value, utcCalendar);
            }
            
            public int compare(Object x, Object y) {
                if (x == null && y == null)
                    return 0;
                else if (x == null)
                    return 1;
                else if (y == null)
                    return -1;
                else {
                    java.sql.Date c1 = (java.sql.Date) x;
                    java.sql.Date c2 = (java.sql.Date) y;
                    return compare(c1, c2);
                }
            }
            
            /*
             * (non-Javadoc)
             * @see org.hibernate.usertype.UserType#hashCode(java.lang.Object)
             */
            public int hashCode(Object x) throws HibernateException {
                return ((java.sql.Date) x).hashCode();
            }
            
            public Object seed(SessionImplementor si) {
                return new java.sql.Date(System.currentTimeMillis());
            }
            
            public Object next(Object current, SessionImplementor si) {
                return new java.sql.Date(System.currentTimeMillis());
            }
        }
    
        /**
         * Like a Hibernate time, but using the UTC TimeZone (not the default TimeZone).
         */
        public static class TimeType extends HibernateUTC {
    
            protected static int[] SQL_TYPES_TIME = { Types.TIME };
    
            /**
             * @see net.sf.hibernate.UserType#sqlTypes()
             */
            public int[] sqlTypes() {
                return SQL_TYPES_TIME;
            }
    
            /**
             * @see net.sf.hibernate.UserType#deepCopy(java.lang.Object)
             */
            public Object deepCopy(Object value) {
                return (value == null) ? null : new java.sql.Time(((Date) value).getTime());
            }
    
            /**
             * @see net.sf.hibernate.UserType#nullSafeGet(java.sql.ResultSet, java.lang.String[],
             *      java.lang.Object)
             */
            public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws SQLException {
                Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
                return rs.getTime(names[0], utcCalendar);
            }
    
            /**
             * @see net.sf.hibernate.UserType#nullSafeSet(java.sql.PreparedStatement, java.lang.Object,
             *      int)
             */
            public void nullSafeSet(PreparedStatement st, Object value, int index) throws SQLException {
                if (!(value instanceof java.sql.Time))
                    value = deepCopy(value);
                Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
                st.setTime(index, (java.sql.Time) value, utcCalendar);
            }
            
            public int compare(Object x, Object y) {
                if (x == null && y == null)
                    return 0;
                else if (x == null)
                    return 1;
                else if (y == null)
                    return -1;
                else {
                    java.sql.Time c1 = (java.sql.Time) x;
                    java.sql.Time c2 = (java.sql.Time) y;
                    return compare(c1, c2);
                }
            }
            
            /*
             * (non-Javadoc)
             * @see org.hibernate.usertype.UserType#hashCode(java.lang.Object)
             */
            public int hashCode(Object x) throws HibernateException {
                return ((java.sql.Time) x).hashCode();
            }
            
            public Object seed(SessionImplementor si) {
                return new java.sql.Time(System.currentTimeMillis());
            }
            
            public Object next(Object current, SessionImplementor si) {
                return new java.sql.Time(System.currentTimeMillis());
            }
    
        }
    
        /**
         * Like a Hibernate timestamp, but using the UTC TimeZone (not the default TimeZone).
         */
        public static class TimestampType extends HibernateUTC {
    
            /**
             * @see net.sf.hibernate.UserType#deepCopy(java.lang.Object)
             */
            public Object deepCopy(Object value) {
                if (value == null)
                    return null;
                java.sql.Timestamp ots = (java.sql.Timestamp) value;
                java.sql.Timestamp ts = new java.sql.Timestamp(ots.getTime());
                ts.setNanos(ots.getNanos());
                return ts;
            }
    
            /**
             * @see net.sf.hibernate.UserType#nullSafeGet(java.sql.ResultSet, java.lang.String[],
             *      java.lang.Object)
             */
            public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws SQLException {
                Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
                return rs.getTimestamp(names[0], utcCalendar);
            }
    
            /**
             * @see net.sf.hibernate.UserType#nullSafeSet(java.sql.PreparedStatement, java.lang.Object,
             *      int)
             */
    
            public void nullSafeSet(PreparedStatement st, Object value, int index) throws SQLException {
                if (!(value instanceof java.sql.Timestamp))
                    value = deepCopy(value);
                Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
                st.setTimestamp(index, (java.sql.Timestamp) value, utcCalendar);
            }
            
            public int compare(Object x, Object y) {
                if (x == null && y == null)
                    return 0;
                else if (x == null)
                    return 1;
                else if (y == null)
                    return -1;
                else {
                    Timestamp c1 = (Timestamp) x;
                    Timestamp c2 = (Timestamp) y;
                    return compare(c1, c2);
                }
            }
            /*
             * (non-Javadoc)
             * @see org.hibernate.usertype.UserType#hashCode(java.lang.Object)
             */
            public int hashCode(Object x) throws HibernateException {
                return ((Timestamp) x).hashCode();
            }
    
            public Object seed(SessionImplementor si) {
                return new Timestamp(System.currentTimeMillis());
                // return new Timestamp(si.getTimestamp());
            }
            
            public Object next(Object current, SessionImplementor si) {
                return new Timestamp(System.currentTimeMillis());
                // return new Timestamp(si.getTimestamp());
            }
        }
    
        public static class CalendarType extends HibernateUTC {
    
            public Class<?> getReturnedClass() {
                return Calendar.class;
            }
    
            /**
             * @see net.sf.hibernate.UserType#deepCopy(java.lang.Object)
             */
            public Object deepCopy(Object value) {
                if (value == null) {
                    return null;
                }
                Calendar c = (Calendar) UTC_CALENDAR.clone();
                c.setTimeInMillis(((Calendar) value).getTimeInMillis());
                return c;
            }
    
            /**
             * @see net.sf.hibernate.UserType#nullSafeGet(java.sql.ResultSet, java.lang.String[],
             *      java.lang.Object)
             */
            public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws SQLException {
                Calendar cal = (Calendar) UTC_CALENDAR.clone();
                Timestamp ts = rs.getTimestamp(names[0], cal);
                if (ts == null || rs.wasNull()) {
                    return null;
                }
                if (Environment.jvmHasTimestampBug()) {
                    cal.setTime(new Date(ts.getTime() + ts.getNanos() / 1000000));
                } else {
                    cal.setTime(ts);
                }
                return cal;
    
            }
    
            /**
             * @see net.sf.hibernate.UserType#nullSafeSet(java.sql.PreparedStatement, java.lang.Object,
             *      int)
             */
    
            public void nullSafeSet(PreparedStatement st, Object value, int index) throws SQLException {
                if (value == null) {
                    st.setNull(index, Types.TIMESTAMP);
                } else {
                    Timestamp t = new Timestamp(((Calendar) value).getTimeInMillis());
                    Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
                    st.setTimestamp(index, t, utcCalendar);
                }
            }
    
            /*
             * (non-Javadoc)
             * @see org.hibernate.usertype.UserType#equals(java.lang.Object, java.lang.Object)
             */
            public boolean equals(Object x, Object y) {
                if (x == y)
                    return true;
                if (x == null || y == null)
                    return false;
    
                Calendar calendar1 = (Calendar) x;
                Calendar calendar2 = (Calendar) y;
    
                return calendar1.getTimeInMillis() == calendar2.getTimeInMillis();
            }
            
            public int compare(Object x, Object y) {
                if (x == null && y == null)
                    return 0;
                else if (x == null)
                    return 1;
                else if (y == null)
                    return -1;
                else {
                    Calendar c1 = (Calendar) x;
                    Calendar c2 = (Calendar) y;
                    return compare(c1, c2);
                }
            }
    
            /*
             * (non-Javadoc)
             * @see org.hibernate.usertype.UserType#hashCode(java.lang.Object)
             */
            public int hashCode(Object x) throws HibernateException {
                return ((Calendar) x).hashCode();
            }
            
            public Object seed(SessionImplementor si) {
                Calendar cal = (Calendar) UTC_CALENDAR.clone();
                cal.setTimeInMillis(System.currentTimeMillis());
                return cal;
            }
            
            public Object next(Object current, SessionImplementor si) {
                Calendar cal = (Calendar) UTC_CALENDAR.clone();
                cal.setTimeInMillis(System.currentTimeMillis());
                return cal;
            }
    
        }
    
        /**
         * Note 071107: passing the static sUTCCalendar instance to the setTimestamp(), getTimestamp()
         * calls above has concurrency issues, as some JDBC drivers do modify the supplied calendar
         * instance. Consequence, the calendar is cloned before use.
         */
    
        /** the Calendar to hold the UTC timezone */
        private static final TimeZone TZ_UTC;
        private static final Calendar UTC_CALENDAR;
        static {
            TZ_UTC = TimeZone.getTimeZone("UTC");
            UTC_CALENDAR = Calendar.getInstance(TZ_UTC);
        }
    
    }
    

     

     

    Some small fixes to the above code...15 May 2009, 15:39ilbmiller
    The above code was very helpful.  I do have a couple of small fixes, and 
    one small enhancement:


    1. The compare methods all have infinite loops.  Change the
            compare(c1, c2)
       lines to
            c1.compare(c2)

    2. Add following to TimestampType definition (willget cast exceptions if
    pass in a Date):

        public Class<?> getReturnedClass()
        {
          return java.sql.Timestamp.class;
        }

    3. And added following to support coercing raw sql result set columns to
    the appropriate values:

    public abstract class HibernateUTC implements UserVersionType, UserType
    {

      // Use to cooerce raw sql queries return columns to appropriate types
      public static final Type DATE = new CustomType(DateType.class, null);
      public static final Type TIME = new CustomType(TimeType.class, null);
      public static final Type TIMESTAMP = new
    CustomType(TimestampType.class, null);
      public static final Type CALENDAR = new CustomType(CalendarType.class,
    null);

      ....

    which is useful for doing something like:

    SQLQuery sql = (SQLQuery)
    session.createSQLQuery(JOB_HISTORY_QUERY).setInteger("jdId",
    jobDefinitionId);
            sql.addScalar("JobId", Hibernate.INTEGER);
            sql.addScalar("ScheduledStartTime", HibernateUTC.CALENDAR);
            sql.addScalar("StartTime", HibernateUTC.CALENDAR);
            sql.addScalar("EndTime", HibernateUTC.CALENDAR);



    Enjoy,
    Brad